第28章 Android虚拟机ART:Dex2Oat、AOT与JIT编译、垃圾回收、内存管理

聊到ART,我得先说说自己的经历。几年前我接手一个性能优化项目,App启动总是慢半拍。查来查去,发现是Dex2Oat在作祟。嗯,从那以后,我对ART的编译机制就格外上心。

ART(Android Runtime)从Android 5.0开始取代Dalvik,成了Android的官方运行时。它最大的变化,就是把原来的解释执行,改成了提前编译(AOT)和即时编译(JIT)混合的模式。说白了,就是让App跑得更快。

28.1 Dex2Oat:从Dex到Oat的转换

Dex2Oat是ART的核心工具之一。它负责把Dex文件(Dalvik Executable)转换成Oat文件(Optimized ART)。Oat文件本质上是ELF格式,里面包含了本地机器码。

我习惯把Dex2Oat比作一个翻译官。它把Java字节码翻译成CPU能直接执行的机器码。这个过程发生在App安装时,或者系统OTA更新后。

核心流程:

  • 输入:Dex文件(classes.dex)
  • 输出:Oat文件(base.oat)
  • 关键参数:指令集(arm64、x86等)、编译过滤器(compiler filter)

编译过滤器是个好东西。它决定了Dex2Oat的编译策略。常见的过滤器有:

过滤器 行为 适用场景
verify 只验证,不编译 调试、开发
quicken 快速编译,部分优化 首次安装
speed 全量编译,性能优先 生产环境
speed-profile 基于Profile的编译 混合编译模式

我曾经踩过一个坑:在低端设备上用了speed过滤器,结果安装时间暴涨,用户直接差评。后来我改成了speed-profile,问题就解决了。

28.2 AOT与JIT编译:两种策略的博弈

AOT(Ahead-Of-Time)编译,就是在安装时把代码全编译好。优点是运行时快,缺点是安装慢、占用空间大。

JIT(Just-In-Time)编译,是在运行时才编译热点代码。优点是启动快、省空间,缺点是运行时会有编译开销。

Android 7.0之后,ART引入了混合编译模式。它结合了AOT和JIT的优点。具体流程是这样的:

  1. App首次安装:只做验证和快速编译(quicken)
  2. App运行:JIT编译热点方法,并记录Profile信息
  3. 设备空闲+充电:后台执行Dex2Oat,根据Profile重新编译
  4. 下次启动:直接使用优化后的Oat文件

我的建议:如果你的App有大量冷启动路径,记得在测试阶段收集Profile。这样Dex2Oat能更精准地编译热点代码。我一般在CI流程里加一步Profile收集,效果很明显。

为什么会这样设计?你想想看,用户最在意的是什么?是安装快、启动快、还是运行流畅?其实都要。混合编译就是在这三者之间找平衡。

28.3 垃圾回收:ART的GC机制

ART的垃圾回收(GC)比Dalvik强了不少。它支持多种GC策略,根据场景动态切换。

主要的GC类型有:

  • Concurrent Mark Sweep (CMS):并发标记清除,适合大堆场景
  • Concurrent Copying (CC):并发复制,减少碎片
  • Generational GC:分代回收,新生代和老年代分开处理

我记得有一次线上OOM频发,查了半天发现是GC策略没选对。那个App有大量短生命周期对象,用CMS反而效率低。换成Generational GC后,GC停顿时间从200ms降到了30ms。

注意:GC停顿(STW,Stop-The-World)是性能杀手。ART虽然做了并发优化,但某些阶段还是得暂停所有线程。比如CMS的重新标记阶段,或者CC的复制阶段。如果你的App有实时性要求,得特别关注这个。

ART的GC还有一个特点:它支持压缩。什么意思呢?就是GC之后,会把存活对象挪到一起,减少内存碎片。这对大内存分配特别友好。

28.4 内存管理:ART的堆与栈

ART的内存管理,说白了就是管好堆(Heap)和栈(Stack)。堆存对象,栈存局部变量和方法调用。

ART的堆分几个区域:

区域 用途 GC行为
新生代(Young) 存放新创建的对象 频繁GC,复制算法
老年代(Old) 存放长期存活的对象 较少GC,标记清除
大对象区域(Large Object Space) 存放超大对象(如Bitmap) 特殊处理

我建议你重点关注大对象区域。Bitmap是内存大户,如果分配不当,很容易触发GC。我曾经优化过一个图片加载库,把大Bitmap直接分配到大对象区域,避免了频繁的Young GC。

栈的管理相对简单。每个线程有自己的栈,大小固定(通常是1MB)。栈溢出一般是因为递归太深或者局部变量太多。嗯,这个在Android开发中不常见,但做NDK开发时得小心。

28.5 知识体系总览

下面这张图,是我整理的ART核心知识体系。你可以把它当作一个快速索引。

ART 运行时 Dex2Oat AOT / JIT 编译 垃圾回收 (GC) 内存管理 Dex → Oat 编译过滤器 AOT 编译 JIT 编译 混合编译 CMS CC 分代GC 堆管理 栈管理 大对象区域 图:ART 运行时核心知识体系

这张图把ART的四个核心模块串起来了。Dex2Oat负责编译,AOT/JIT决定编译时机,GC管回收,内存管理管分配。它们互相配合,共同决定了App的性能表现。

我个人觉得,理解ART的关键不在于记住每个细节,而在于把握它的设计思想:在性能、空间、时间之间做权衡。你想想看,从Dalvik到ART,从纯AOT到混合编译,每一步都是在找那个最优解。

好了,这一章就聊到这儿。如果你在实际项目中遇到ART相关的问题,欢迎随时交流。毕竟,纸上得来终觉浅,绝知此事要躬行。


公众号:蓝海资料掘金营,微信deep3321