什么是 GC 抖动?它对帧率的影响机制是什么?

解读

在国内 Android 面试中,性能题是“必考项”,而 GC 抖动又是性能题里的“高频陷阱”。面试官问这道题,通常想验证三件事:

  1. 候选人是否真正理解 Dalvik/ART 的内存回收原理,而不是只会背“Stop-The-World”;
  2. 能否把 GC 与 16 ms 帧率红线、VSync 信号、掉帧三者之间的因果关系讲清楚;
  3. 是否具备线上定位与治理 GC 抖动的实战经验(Systrace/Perfetto 看“垃圾回收”线程、看“Janky frames”)。
    回答时务必用“现象→根因→证据→治理”四段式,既体现理论深度,也展示落地能力。

知识点

  1. GC 抖动(GC Jank)定义:在单帧 16.6 ms 周期内,由于垃圾回收器暂停应用线程(Stop-The-World,简称 STW),导致 Choreographer 无法在 VSync 信号到来时及时绘制,从而出现掉帧、卡顿的现象。
  2. ART 回收器演进:
    • 5.0 以前 Dalvik:标记-清除,全程 STW,抖动明显;
    • 5.x~8.x ART:CMS(并发标记清除)+ 压缩,减少但无法避免 STW;
    • 9.0+ ART:分代+并发复制(Generational Concurrent Copying),STW 缩短到 <3 ms,但大对象分配或内存濒临上限时仍可能触发“Full GC”,一次 20 ms+,直接拖垮帧率。
  3. 触发时机:
    • 每分配一次对象,ART 会检查“分配限额”与“堆余量”,一旦不足就触发局部 GC;
    • 当堆利用率达到“并发 GC 启动阈值”时,后台线程开始并发标记,但若主线程继续高速分配,仍可能被迫等待;
    • 当堆几乎满时,进入“临界状态”,任何线程再分配都会触发“同步 Full GC”,此时主线程完全阻塞,卡顿肉眼可见。
  4. 帧率影响链路:
    VSync→Choreographer→doFrame→draw→renderThread→GPU;
    若 GC STW 落在 doFrame 阶段,draw 未完成,SurfaceFlinger 拿不到新 Buffer,本次 VSync 周期被跳过,用户看到“一帧重复”,即一次 Jank;连续多次即肉眼卡顿。
  5. 国内典型场景:
    • 首页 RecyclerView 一次性拉取 200 条 Feed,Item 布局层级深,瞬间分配上万对象;
    • 图片列表未做预加载与对象池,onBind 里频繁 new Drawable;
    • 直播弹幕或股票 Tick 流,每秒上千次 StringBuilder.toString(),短生命周期对象暴涨。
  6. 定位工具:
    • Systrace:看“HeapTaskDaemon”线程大块橙色“Marking”段,若与“Choreographer#doFrame”重叠,即可判定 GC 抖动;
    • Perfetto:量化 STW 时长,看“art::gc::collector::Run”事件;
    • Memory Profiler:观察“Allocated”瞬间跳升与“GC”图标;
    • 线上:Firebase / 字节埋点 / 友盟+ 自定义“慢帧”日志,堆栈采样过滤“art/runtime/gc”关键字。
  7. 治理手段:
    • 对象复用:RecyclerView ViewPool、OkHttp ConnectionPool、LruCache、Glide BitmapPool;
    • 减少临时对象:Kotlin 内联 + 高阶函数避免匿名内部类,StringBuilder 复用,replace 用 StringPool;
    • 减少自动装箱:IntArray 代替 List<Integer>
    • 图片内存归一:RGB_565、inSampleSize、webp;
    • 后台线程预分配:提前在子线程构造好数据,主线程只 set;
    • 大对象隔离:Native 层或 mmap 管理,避免进入 Java 堆;
    • 动态降级:低端机关闭动画、减少预加载;
    • 兜底:监控 GC 次数与耗时,超过阈值自动上报并触发“内存兜底策略”(清缓存、降帧率)。

答案

GC 抖动是指垃圾回收器在执行 Stop-The-World 暂停时,阻塞了应用主线程,导致当前帧无法在 16.6 ms 内完成绘制,从而出现掉帧卡顿的现象。
其影响帧率的核心机制是:当 VSync 信号到来,Choreographer 回调 doFrame 开始测量、布局、绘制,若此时 GC 正在标记或复制对象,主线程被挂起,renderThread 无法及时提交 GPU,SurfaceFlinger 只能复用上一帧 Buffer,用户便感知为“卡了一下”。
线上定位时,可在 Systrace 中观察 HeapTaskDaemon 的橙色段与 doFrame 的重叠情况,若单次 STW 超过 7 ms,基本可判定为 GC 抖动。
治理思路分三层:

  1. 减少分配——对象池、缓存、避免自动装箱;
  2. 降低回收压力——图片降质、大对象外移、分帧加载;
  3. 监控兜底——线上慢帧日志 + 动态降级,确保低端机体验可控。

拓展思考

  1. 为什么 Android 10 之后 GC 抖动少了,但“大对象分配”仍可能引爆卡顿?
    答:并发复制回收器把 STW 压缩到 <3 ms,可一旦 Java 堆碎片过多或接近最大堆,会回退到“同步压缩”,一次 30 ms+,瞬间掉 2 帧。
  2. Kotlin Coroutines 的“协程栈”是分配在 Java 堆还是 Native 堆?频繁创建 Coroutine 会不会引发 GC 抖动?
    答:默认 Java 堆;若 launch 无限制,每次创建 Continuation 对象,仍可能触发局部 GC。官方建议配合 CoroutineScope + 有限线程池,并在低端机限制并发数量。
  3. 国内厂商 ROM 对 GC 参数做过哪些“魔改”?
    答:华为/小米在 init.rc 里调大了“heapgrowthlimit”与“gc.concurrent-mark-start”,让并发 GC 更早启动,减少 Full GC 概率;但副作用是后台应用占内存更高,需权衡杀后台策略。面试时可结合“厂商兼容性”话题,展示自己对国内碎片化环境的深度理解。