ART 是 Android 5.0 之后取代 Dalvik 的运行时,它在安装期或首次启动时把 DEX 字节码编译为机器码,同时也在垃圾回收上做了根本性调整。ART 的目标不只是减少单次 GC 的暂停时间,还希望 GC 能更好地配合多核设备,尽量避免在应用主线程执行需要长时间挂起的回收阶段。为此,ART 把堆划分成不同区域,并根据对象年龄、大小和是否可移动来选择对应的回收算法。

ART 堆空间与对象分配
ART 的 Java 堆并不是一整块连续内存,而是由多个 space 组成。常见的有 image space、zygote space、non-moving space 和 main space。启动时从 zygote 继承的对象会放在 zygote space 中,这部分对象通常被认为是不可移动的,因为一旦移动就要更新大量共享页。镜像空间 image space 保存预加载的框架类,加载后以只读方式映射,能减少内存占用。应用运行过程中新分配的小对象大多进入 main space 的分配缓冲区,线程本地缓冲区用完后再从堆中申请。
对象分配时,ART 会优先使用线程本地的 TLAB。TLAB 是指每个线程独立持有的一块小缓冲区,分配动作只需要移动指针,不需要加锁。只有在 TLAB 耗尽后,线程才会进入慢速分配路径,由堆分配器从 main space 中划出新的缓冲区。这种机制显著减少了并发分配时的竞争,但也意味着如果某个线程频繁分配大对象,或者连续创建生命周期很短的对象,会更快触发 GC。大对象通常会直接进入 large object space,因为这些对象移动成本高,不适合放在普通的可移动区域。
堆空间划分还影响回收策略。对于不可移动的对象,只能使用标记清除或标记压缩中的标记阶段,而不能使用复制算法搬移对象。对于可移动区域,ART 可以通过复制回收把存活对象集中到一块,释放整片空间。理解对象存放在哪个 space,有助于判断 GC 日志中出现的 region 名称和回收成本。
ART 常用垃圾回收算法
ART 提供多种 GC 策略,核心包括并发标记清除、并发复制和并发压缩。标记清除的优势是实现简单,不需要移动对象,适合不可移动区域或大对象空间。其过程分两个阶段:从 GC roots 出发遍历对象图标记存活对象,然后清除未被标记的对象。ART 会尽量让标记阶段与应用线程并发执行,只在开始和结束阶段短暂暂停所有线程,以处理引用更新和写屏障。
复制回收把存活对象从一个半区复制到另一个半区,复制完成后直接重置原半区,分配速度快且不会产生碎片。代价是只能使用堆的一半作为有效空间,比较适合存活对象少、垃圾比例高的年轻代。ART 在堆空间设计上通过 region 支持复制回收,不必把整个堆一分为二。并发复制还需要读写屏障来保证应用线程在移动对象时仍能正确访问旧地址,ART 使用 Baker 风格的读屏障实现这一能力。
压缩回收则是标记整理的结合,它在标记结束后把存活对象向一侧移动,更新引用并回收剩余空间。与标记清除相比,压缩能消除碎片,但移动对象需要短暂停顿或使用并发方式维护转发指针。ART 在堆碎片严重或老年代空间不足时会选择压缩回收。不同回收器的切换由运行时根据暂停时间、可用内存和碎片情况动态决定,而不是固定使用某一种。
代码示例展示了 Java 层如何触发对象分配与释放,配合 profiler 可以观察 GC 频率:
public class GcDemo {
private static final int LOOP_COUNT = 100000;
public static void main(String[] args) {
for (int i = 0; i < LOOP_COUNT; i++) {
byte[] data = new byte[1024];
process(data);
}
}
private static void process(byte[] data) {
if (data.length > 0) {
data[0] = 1;
}
}
}
上述代码在循环中反复创建 1KB 的 byte 数组,如果这些数组很快变成垃圾,就会引起频繁的 minor GC。真实开发中需要避免在 onDraw、onMeasure 等高频回调里创建大量临时对象,否则可能造成内存抖动。
GC 触发时机与日志解读
ART 触发 GC 的条件主要有三类:分配失败、堆占用达到阈值、显式调用 System.gc。分配失败是最常见的触发点。当线程无法在 TLAB 或堆中分配新对象时,运行时会在执行分配前启动 GC,试图释放空间。堆占用达到阈值则可能由后台线程触发并发 GC,以减少应用线程停顿。显式调用 System.gc 在 ART 上虽然会执行 GC,但不建议依赖它管理内存,因为其回收时机和类型不完全可控。
查看 GC 日志是分析性能问题的第一步。在 Android 10 之前,可以通过 logcat 中的 dalvikvm 和 art 标签看到类似 dalvikvm: GC_FOR_ALLOC freed 2048K, 45% free 这样的记录。日志中通常会包含 GC 原因、释放内存量、暂停时间等信息。通过命令 adb logcat -s art 可以过滤 ART 输出,观察应用运行期间发生的是并发 GC 还是阻塞式 GC。只要日志中出现较长的 pause time,就说明主线程可能因为等待 GC 而卡顿。
adb logcat -s art # 观察类似输出: # art : Starting a blocking GC Explicit # art : WaitForGcToComplete blocked for 8.120ms
如果日志中频繁出现 GC_FOR_ALLOC,说明应用分配速度过快,当前堆空间不够。此时可以考虑扩大堆参数或降低对象分配频率。借助 CPU Profiler 和 Memory Profiler 还可以看到分配热点,确认是哪些线程在持续产生垃圾。对于线上环境,也可以通过自定义性能监控接口上报 GC 次数和停顿时长,但要注意控制采样频率,避免监控本身带来额外开销。
降低 GC 压力的实践方法
减少 GC 影响最有效的办法是少创建不必要的对象。使用对象池复用昂贵的对象,例如 Message、Bitmap 和自定义的缓存类。Message 可以通过 Handler.obtainMessage 获取,Bitmap 在频繁加载图片时配合 inBitmap 复用内存。对于简单数据结构,尽量使用基本类型数组或 SparseArray,避免自动装箱。数值计算中的 Integer 和 Double 包装类型,每一层装箱都会产生短生命周期对象。
另一个方向是控制大对象的分配。大对象超过一定阈值后会直接进入 large object space,因为它移动成本高,几乎不会参与复制回收。频繁创建大对象会快速耗尽该区域并触发 GC。例如在解码图片时,如果每次都创建新的 Bitmap,内存压力会很快上来。正确的做法是根据显示尺寸计算采样率,并复用已有 Bitmap。列表和字符串拼接也是常见问题,字符串拼接在 Java 中会生成新的 String 对象,可以使用 StringBuilder 减少中间对象。
对堆参数的使用应保持克制。AndroidManifest 中的 android:largeHeap 只适合确实有大量内存需求的场景,不能用来掩盖内存泄漏。打开 largeHeap 后堆更大,但回收时需要遍历的对象也更多,单次 GC 的时间可能更长。真正的问题应该通过 Memory Profiler 查看对象数量、引用链和 GC 频率,定位哪些代码路径在持续产生垃圾,而不是单纯扩大堆。
ART 的垃圾回收设计已经显著优于旧版 Dalvik,但应用层的分配行为仍然直接决定 GC 频率和停顿表现。只有理解堆空间、回收算法和触发日志,才能在实际项目中做出有依据的内存优化。
Android ART垃圾回收GC修改时间:2026-09-28 08:47:40