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内存优化的基本功