3、ART虚拟机内存管理:ART与Dalvik的区别、ART的预编译与AOT、ART的GC优化
聊到Android内存管理,ART虚拟机是个绕不开的话题。我记得Android 4.4刚推出ART时,很多人还把它当成一个「实验性功能」来尝鲜。到了5.0,ART正式取代Dalvik,成为默认运行时。这一换,背后其实是整个内存管理思路的转变。
说白了,Dalvik和ART最大的区别就一句话:Dalvik是边解释边执行,ART是提前编译好再跑。这个差异,直接影响了应用的启动速度、运行流畅度,还有内存的占用方式。
ART与Dalvik的核心区别
先说说Dalvik。它用的是JIT(Just-In-Time)编译。应用每次启动时,Dalvik会把热点代码实时编译成本地机器码。好处是安装快、占空间小,坏处也很明显——每次启动都要「现编现用」,CPU开销大,启动慢。
ART则引入了AOT(Ahead-Of-Time)预编译。应用安装时,ART就把Dex字节码一次性编译成OAT文件(其实就是ELF格式的本地机器码)。运行时直接执行,省去了编译这一步。
一个直观的对比:
| 对比维度 | Dalvik(JIT) | ART(AOT) |
|---|---|---|
| 安装速度 | 快(无需编译) | 慢(需要预编译) |
| 首次启动 | 慢(边解释边执行) | 快(直接执行机器码) |
| 运行时内存 | 需要额外JIT缓存 | 代码段直接映射,更省内存 |
| 包体积 | 小(只有Dex) | 大(多了OAT文件) |
| GC效率 | 分代GC,暂停时间较长 | 并发标记+紧凑,暂停更短 |
我在项目中遇到过一个问题:某款低端机升级到Android 5.0后,系统分区空间急剧减少。查了半天,发现就是ART预编译生成的OAT文件占用了大量空间。嗯,这就是AOT的代价——用空间换时间。
ART的预编译与AOT机制
ART的AOT编译发生在应用安装时。系统会调用dex2oat这个守护进程,把Dex字节码翻译成本地指令。这个过程会生成一个.oat文件,本质上是一个动态库。
你想想看,这个OAT文件里装了什么?它包含了编译后的方法代码、类元数据、还有原始的Dex数据。运行时,ART会把OAT文件通过mmap映射到进程地址空间。代码段是只读的,可以被多个进程共享——这一点对内存优化非常关键。
// 查看OAT文件信息的命令
adb shell dumpsys meminfo <package_name>
// 或者直接看oat文件大小
adb shell ls -l /data/app/<package>/oat/arm64/
不过,纯AOT也有它的痛点。Android 7.0之后,ART引入了混合模式:解释执行 + 后台编译 + 配置文件引导。说白了就是,安装时只做轻量级的验证和优化,不全部编译。应用运行时,系统会记录热点方法,等设备空闲时再通过dex2oat后台编译。
我的个人习惯:在做系统级性能优化时,我会关注/data/misc/profiles/目录下的prof文件。这些文件记录了应用的热点方法,是ART做配置文件引导编译的依据。如果发现某个应用启动慢,可以先检查它的prof文件是否完整。
ART的GC优化
GC(垃圾回收)是ART内存管理的重头戏。Dalvik的GC用的是CMS(Concurrent Mark Sweep)算法,虽然也是并发,但它在标记阶段会触发「世界暂停」(World Stop)。我调试过一台老设备,GC暂停时间经常超过100ms,直接导致界面掉帧。
ART在这方面做了几个关键改进:
- 并发标记(Concurrent Marking):ART的标记阶段几乎全程并发,只有很短的暂停窗口。它利用读写屏障(Read/Write Barrier)来追踪对象引用变化,不需要像Dalvik那样反复暂停。
- 紧凑(Compaction):ART引入了移动垃圾回收器。它能把分散的内存碎片整理到一起,减少内存碎片化。这一点在长时间运行的应用上特别明显。
- 分代收集:ART把堆分成了年轻代和老年代。新对象分配在年轻代,GC频率高但速度快。熬过几次GC的对象晋升到老年代,GC频率降低。
ART GC的三种回收器:
- Concurrent Copying (CC):默认回收器,使用读屏障,暂停时间极短(通常<5ms)
- Concurrent Mark Sweep (CMS):老版本使用,暂停时间较长
- Generational Concurrent Copying (GCC):Android 10引入,结合分代和CC,效率更高
我曾经遇到过一个问题:某款游戏在运行30分钟后,帧率从60fps掉到30fps。用systrace抓了一下,发现是ART的GC频繁触发,每次暂停8-10ms。为什么会这样?
排查后发现,游戏每帧都会创建大量临时对象,导致年轻代迅速填满。GC虽然暂停时间短,但频率太高,累积起来就影响了帧率。解决方案是:优化对象池,减少临时分配。同时调整了ART的堆参数,把年轻代大小从8MB提升到16MB,GC频率明显下降。
避坑指南:我曾经在调整ART GC参数时踩过坑。比如dalvik.vm.heapstartsize设得太大,虽然减少了GC次数,但应用启动时直接分配了上百MB内存,导致低端机直接OOM。建议从小值开始逐步调优,不要一上来就翻倍。
ART内存管理的几个关键参数
做系统级优化时,这几个参数我经常调整:
# 堆起始大小
dalvik.vm.heapstartsize=8m
# 堆最大大小
dalvik.vm.heapsize=256m
# 堆增长上限
dalvik.vm.heapgrowthlimit=192m
# 年轻代占比
dalvik.vm.heapminfree=2m
dalvik.vm.heapmaxfree=8m
这些参数写在/system/build.prop里。调整后需要重启才能生效。我个人建议,在调优时先用dumpsys meminfo观察应用的内存使用模式,再针对性地调整。
总结一下
ART相比Dalvik,最大的进步在于:预编译减少了运行时开销,并发GC降低了暂停时间,紧凑机制缓解了内存碎片。但AOT带来的空间膨胀问题,以及GC频率过高导致的性能抖动,依然是我们做性能优化时需要重点关注的。
嗯,ART的内存管理其实还有很多细节,比如Large Object Space、Zygote堆的共享机制等等。这些内容我们后面章节再展开聊。记住一句话:理解ART的GC行为,是做好Android内存优化的基本功。