Administrator
发布于 2026-09-22 / 3,327 阅读
0
0

ART 运行时深入:AOT、JIT、PGO 与 GC

前言

"为什么我的 App 冷启动慢"——这个问题如果只回答"用 Baseline Profiles",那等于没回答。真正要理解,得钻进 ART(Android Runtime)内部看看它到底怎么跑你的字节码。

这一篇讲三件事:ART 的编译策略(AOT/JIT/PGO 怎么配合)、垃圾回收器怎么选(为什么不用 G1)、以及实际开发中怎么利用这些知识做性能优化。

一、ART 的三层编译

ART 不是简单的"解释器"或"AOT 编译器",它是三者的混合体:

                +-------------------+
                |   dex 字节码       |
                +---------+---------+
                          |
                   +------+------+
                   |  解释器执行   |  <-- 启动时所有方法走这里
                   +------+------+
                          |
                   profile 采集
                   (热点方法)
                          |
            +-------------+-------------+
            |                           |
    +-------+-------+           +------+------+
    |   JIT 编译    |           |  AOT 编译   |
    |  (运行中)     |           | (安装/空闲)  |
    +-------+-------+           +------+------+
            |                           |
            +-------------+-------------+
                          |
                   Profile 持久化
                   下次安装 -> AOT 预编译

解释器(Interpreter)

App 启动那一刻,所有方法都走解释器逐条执行字节码。慢,但不需要编译时间。这保证冷启动不会有额外的编译开销。

JIT(Just-In-Time)

解释器执行过程中,ART 会记录哪些方法被调得频繁(默认阈值 100 次)。达到阈值后,JIT 编译器在后台线程把这个方法编译成机器码,后续调用直接走机器码。

JIT 编译结果不持久化,App 重启后清空。

AOT(Ahead-Of-Time)

安装 App 时(或空闲时),ART 会用 dex2oat 工具把字节码预编译成机器码(OAT 文件)。启动时直接加载,不走解释器。

但全量 AOT 编译太慢(安装时间过长),所以 Android 7.0+ 改成了"混合编译":只 AOT 编译 Profile 里标记的热点方法。

PGO(Profile-Guided Optimization)

PGO 是 AOT 和 JIT 的桥梁。运行时 JIT 采集热点方法信息,存成 profile 文件。下次安装/更新时,dex2oat 拿 profile 做输入,只 AOT 编译热点方法。

第 1 次运行: 解释器 -> JIT 编译热点 -> 存 profile
第 2 次启动: AOT 加载 profile 标记的方法 -> 启动快

Baseline Profiles 就是人工提供这份 profile,跳过"运行时采集"这一步。

二、三种策略的配合关系

编译策略

触发时机

产物

持久化

适用场景

解释器

方法首次执行

无

否

所有方法的入口

JIT

运行中,热点方法

机器码(内存)

否

快速优化频繁调用的方法

AOT

安装/空闲

OAT 文件

是

预编译,启动加速

Android 各版本的策略变化:

版本

策略

说明

5.0-6.0

纯 AOT

安装时全量编译,安装慢

7.0-9.0

JIT + AOT(PGO)

运行时采集,空闲时 AOT

10+

混合 + Baseline

支持人工提供 profile

14+

JIT 增强

JIT 优化能力提升

三、GC:为什么 ART 不用 G1

ART 在 Android 10 之前用的是 Concurrent Copying Collector(CC),10 之后做了增强,叫 CC(Generational)。不是 G1,不是 ZGC,是自研的。

为什么不用现成的 G1/ZGC?

  1. ART 的堆是 Zygote 预分配的大块连续内存,不需要 G1 的分区设计

  2. 移动设备对暂停时间极度敏感(16ms 内必须完成),CC 用 read barrier 方式做并发标记,暂停可以做到 5ms 以下

  3. G1 的 Region 设计对大堆(4GB+)更有意义,移动端堆一般 256MB 以下

CC 的工作方式:

1. 标记阶段(并发): GC 线程遍历对象图,标记存活对象
2. 复制阶段(STW < 5ms): 把存活对象复制到新空间
3. 修正阶段(并发): 修正指向旧地址的引用

关键设计:read barrier。每次读对象引用时,ART 检查这个引用是否指向已经复制过的旧地址。如果是,修正为新地址。这让 GC 不需要 STW 整个复制阶段。

实际影响:你几乎不会看到 ART 的 GC 日志里出现"Full GC"或 >10ms 的暂停。但如果你的代码大量分配临时对象(比如循环里 new),GC 频率上升,暂停次数多了也会累积影响。

四、实战:利用 ART 知识做优化

场景 1:冷启动慢

// 坏例子:启动时创建大量 lambda
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        // 每个 lambda 都是一个新对象
        repeat(50) { i ->
            workerScheduler.execute {
                loadFeature(i)
            }
        }
    }
}

// 好例子:延迟初始化 + 减少 lambda
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        // 用 App Startup 框架管理
        initializerProvider.deferInit {
            loadCriticalFeatures()
        }
        // 非关键路径延迟到空闲
        mainHandler.postDelayed({
            loadNonCriticalFeatures()
        }, 1000)
    }
}

原理:启动阶段所有方法走解释器,JIT 还没生效。减少方法调用次数和对象分配,直接减少解释器执行时间。

场景 2:运行中卡顿

// 检测 GC 频率
// adb shell dumpsys meminfo <package> | grep "GC"

// 输出示例:
// GC in 1s: 23 times, total pause: 89ms
// 如果 1 秒内 GC >10 次,说明对象分配太频繁

// 优化:复用对象
val buffer = ByteArray(4096)  // 复用

fun processData(input: InputStream) {
    while (input.read(buffer) > 0) {
        // 不在循环里 new ByteArray
        handleChunk(buffer)
    }
}

场景 3:Baseline Profile 不生效

// 确认 profile 是否加载
// adb shell cmd package compile -m speed-profile -f <package>

// 检查 dex2oat 日志
// adb logcat | grep "dex2oat"
// 看是否有 "Compiled methods: XXX" 输出

常见原因:

  • Profile 文件没打进 APK(检查 assets/dexopt/baseline-prof 路径)

  • Profile 里的类名和方法签名跟实际不匹配(混淆后 R8 改了名)

  • 设备存储空间不足,dex2oat 跳过编译

五、编译模式对照

# 查看当前 App 的编译模式
adb shell cmd package list packages -f | grep <package>

# 编译模式选项
# -m verify: 只验证,不编译(最慢)
# -m quicken: 快速编译,不做优化
# -m speed: 全量 AOT 编译
# -m speed-profile: 按 profile 编译(推荐)

模式

安装时间

运行速度

磁盘占用

verify

最快

最慢

最小

quicken

快

慢

小

speed

最慢

最快

最大

speed-profile

中等

快

中等

Google Play 安装 App 时默认用 speed-profile(如果你提供了 Baseline Profile)。国内应用市场很多用 quicken 或 verify,这是国内 App 启动普遍慢的原因之一。

小结

ART 的三层编译(解释器 -> JIT -> AOT)配合 PGO,兼顾了启动速度和运行性能。GC 用 CC + read barrier,暂停控制在 5ms 以下。

理解这些机制的实际价值:你能知道"为什么慢"和"怎么验证是否优化生效",而不是盲目加 Profile 和改代码。

下篇讲 Compose 类型安全导航,从字符串路由迁移到编译时检查。


评论