10、ART GC优化:Generational GC,Compact GC,GC调优参数

聊到ART的GC优化,我得先坦白一件事。早年我刚从Dalvik切到ART时,心里其实有点抵触——"不就是换个运行时嘛,GC能有多大差别?"结果第一次线上OOM问题排查,就让我彻底改观了。ART的GC,说白了,是真正为现代Android系统量身定做的。

10.1 为什么ART要搞Generational GC?

先问个问题:你写代码时,大部分对象是活多久?

答案很明确——绝大多数对象都是"朝生暮死"的。比如一个点击事件里new出来的临时对象,方法执行完就没人用了。这种现象叫"弱代际假设"(Weak Generational Hypothesis)。

Dalvik的GC是"一视同仁"的,每次扫描所有对象。这就像你每次打扫房间都要翻箱倒柜——效率太低了。ART的Generational GC(分代GC)就是针对这个痛点设计的。

核心思路:把堆内存分成年轻代(Young Generation)和老年代(Old Generation)。新对象先分配在年轻代,GC时只扫年轻代。对象熬过几次GC后,晋升到老年代。

我在项目中遇到过这样一个场景:一个图片列表页面,每次滑动都会创建大量Bitmap相关的临时对象。Dalvik下GC频繁触发,卡顿明显。换成ART的Generational GC后,年轻代回收效率极高,大部分临时对象在年轻代就被干掉了,老年代几乎不动。用户体验提升了一个档次。

10.2 Generational GC的工作流程

ART的分代GC,我习惯把它拆成三个阶段来看:

  1. 分配阶段:新对象直接分配在年轻代的"伊甸园"(Eden)区域。空间不够时触发年轻代GC。
  2. 晋升阶段:对象在年轻代存活过一次GC后,进入"幸存者区"(Survivor)。多次存活后,晋升到老年代。
  3. 混合GC:老年代空间紧张时,触发Full GC,同时扫描年轻代和老年代。

你想想看,这个设计最妙的地方在哪?——大部分GC都只发生在年轻代,耗时极短。用户几乎感知不到。

我的经验:如果你在Logcat里看到"AllocSpace young"相关的GC日志,说明触发的是年轻代GC。这种GC通常耗时在几毫秒以内,不用太紧张。但如果频繁出现"Concurrent"或"Full" GC,那就要警惕了。

10.3 Compact GC:内存碎片的天敌

说到Compact GC(压缩GC),我得先吐槽一下Dalvik的一个老大难问题——内存碎片。

我曾经维护过一个老项目,跑在Android 4.4上。应用运行时间一长,明明总内存还有剩余,但就是分配不出一个连续的大对象。为什么?因为内存被碎片化了。就像你有一堆零钱,但凑不出一张整钞。

ART的Compact GC就是来解决这个问题的。它的核心机制是:

  • 移动对象:把存活对象往堆的一端移动,腾出连续的空闲空间
  • 更新引用:移动后,所有指向该对象的引用都要更新
  • 减少碎片:压缩后,大对象分配成功率大幅提升

ART支持两种压缩模式:

模式 触发条件 特点
半空间压缩(Semi-Space) 后台自动触发 复制存活对象到另一个空间,原空间全部回收
标记-压缩(Mark-Compact) Full GC时触发 原地压缩,不额外占用空间

注意:Compact GC虽然能解决碎片问题,但它是有代价的——移动对象需要STW(Stop-The-World)。虽然ART做了大量优化(比如并发标记),但如果压缩范围太大,还是可能引起卡顿。我曾经在低端机上遇到过压缩GC导致掉帧的情况,后来通过调整触发阈值解决了。

10.4 GC调优参数:实战中的"旋钮"

ART暴露了一些GC调优参数,虽然普通开发者很少直接改,但理解它们对排查问题很有帮助。我挑几个最常用的说说:

10.4.1 堆大小相关

  • -Xms / -Xmx:初始堆大小和最大堆大小。我建议不要设太大,否则GC压力会集中在一次Full GC上。
  • -XX:HeapGrowthLimit:堆增长上限。防止某个页面突然申请大量内存。
  • -XX:HeapMinFree / -XX:HeapMaxFree:控制堆空闲空间的上下限。设得太小会导致GC频繁触发。

10.4.2 GC触发相关

  • -XX:GCTriggerRatio:当已用内存达到堆的某个比例时触发GC。默认是75%。我习惯调高到85%,减少不必要的GC。
  • -XX:ConcGCThreads:并发GC的线程数。多核设备上可以适当增加。

10.4.3 分代相关

  • -XX:YoungGenerationSize:年轻代大小。如果应用创建大量临时对象,可以适当增大。
  • -XX:PromotionRatio:对象晋升到老年代的阈值。调高可以减少晋升,但会增加年轻代GC次数。

避坑指南:我曾经在一个视频播放器项目里,为了减少GC把堆设得特别大(-Xmx512m)。结果呢?GC间隔是长了,但每次触发都是Full GC,卡顿更明显了。后来我改成适当缩小堆,配合Generational GC,效果反而更好。记住:不是堆越大越好,关键是GC效率。

10.5 实战:如何观察和优化GC行为

说了这么多理论,来点实际的。我一般用这几个方法观察GC:

  1. adb shell dumpsys meminfo:看堆的总体使用情况,包括年轻代和老年代占比。
  2. Logcat过滤"GC":看GC的类型、耗时、回收量。重点关注"Concurrent"和"Full"。
  3. Profile with Android Studio:Memory Profiler可以直观看到GC频率和堆变化。

举个例子,有一次我排查一个列表滑动卡顿的问题。通过Logcat发现,每次滑动都会触发"AllocSpace young" GC,耗时2-3ms。虽然单次不长,但频率太高(每秒触发十几次),累积起来就卡了。

怎么优化?我做了两件事:

  • 减少滑动时的对象创建(比如复用ViewHolder、使用对象池)
  • 适当增大年轻代,让临时对象有更多空间"存活"

优化后,GC频率降到了每秒2-3次,卡顿基本消失。

10.6 一张图总结ART GC优化

嗯,说了这么多,我画张图帮你理清思路:

ART GC优化核心逻辑 Generational GC Compact GC 调优参数 年轻代快速回收 对象晋升机制 减少Full GC频率 半空间压缩 标记-压缩 解决内存碎片 堆大小控制 GC触发阈值 分代大小调整 目标:低延迟 + 高吞吐 + 无碎片

这张图把ART GC优化的三个核心方向串起来了。Generational GC解决"回收效率"问题,Compact GC解决"内存碎片"问题,调优参数则是你手里的"调节旋钮"。三者配合,才能让应用跑得又快又稳。

最后说一句:GC调优不是一锤子买卖。不同设备、不同Android版本、不同应用场景,最优参数都不一样。我的习惯是:先跑一遍Profile,找到瓶颈,再针对性地调一两个参数。别一上来就改一堆,否则你都不知道哪个参数起了作用。

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