增量标记通过将全堆标记拆分为毫秒级小任务,在应用线程间隙穿插执行,依赖三色标记模型与写屏障保障正确性;它缓解单次STW停顿,但无法解决高频对象创建引发的GC抖动。

增量标记(Incremental Marking)是现代垃圾回收器为降低 Stop-the-world(STW)停顿而采用的核心技术,它把原本一次性的全堆标记过程拆分成多个小步,在应用线程运行间隙穿插执行。理解它,关键在于抓住“分时、并发、写屏障”三个要点;规避高频对象创建引发的执行抖动,则需从源头控制对象生命周期,而非仅依赖 GC 优化。
增量标记如何工作:不是“慢一点”,而是“错开时间”
传统标记-清除需暂停所有用户线程,遍历整个堆完成标记,STW 时间随堆大小线性增长。增量标记将其分解为多个毫秒级小任务,每次只扫描一部分对象图(如一个页或一组对象),并在两次扫描之间交还 CPU 给应用线程继续运行。
- 标记过程被划分为“标记根集→扫描灰色对象→转移至黑色”的循环,每次只推进少量灰色节点
- 应用线程在运行中修改对象引用时,会触发写屏障(Write Barrier),确保新产生的引用关系不被漏标——这是增量能正确工作的前提
- 标记进度由 GC 控制器动态调节:若应用分配压力大,就多让出时间给标记;若空闲,则加快标记节奏,避免延迟回收
为什么高频对象创建仍会导致抖动?增量标记也救不了
增量标记缓解的是“标记耗时长”带来的卡顿,但无法消除“分配太猛”引发的连锁反应。当每毫秒都在 new 出数百个 String、POJO 或 List 时:
- 年轻代快速填满,触发频繁 Minor GC,即使每次只停顿几毫秒,叠加起来就是持续掉帧
- 大量短命对象涌入老年代(如逃逸分析失效、TLAB 不足),迫使 CMS 或 G1 提前启动并发周期,反而增加 CPU 开销
- 内存碎片加剧,尤其在 native 堆(如 Android Bitmap)中,分配失败可能直接触发同步 full GC
真正有效的规避方式:切断抖动源头
不靠 GC 调优,而靠代码层约束对象生成节奏与复用路径:
- 复用不可变中间态:如 SimpleDateFormat、DecimalFormat 等格式化工具类,声明为 static final 或使用 ThreadLocal 缓存,禁止在 onBindViewHolder/onDraw 中重复构造
- 预分配 + 对象池:对固定结构临时对象(如网络请求 DTO、日志事件体),用 sync.Pool(Go)、ObjectPool(Kotlin)、Pools.SimplePool(Android)管理,Get/Recycle 成为标准流程
- 延迟计算 & 懒加载:避免在循环内拼接字符串(用 StringBuilder.append 替代 +)、避免提前解析 JSON 字段(用 JsonReader 流式读取按需解析)
- 栈上分配替代(JVM 可选):对极小、无逃逸对象(如 Point、Rect),启用 JVM 的 Escape Analysis(-XX:+DoEscapeAnalysis)可自动转为栈分配,完全绕过 GC
验证是否生效:看 GC 日志,而不是看 FPS
抖动修复效果不能只凭界面是否流畅判断。应捕获真实场景下的 GC 日志,关注两个硬指标:
- 单位时间内(如 1 秒)GC 次数是否从 10+ 降至 2–3 次
- 每次 GC 的 “Alloc” 量是否显著下降(例如从平均 4MB → 0.5MB),说明对象创建密度已受控










