cms采用增量更新,g1采用原始快照;前者在黑色对象新增指向白色对象的引用时将其重新标记为灰色,后者在灰色对象删除指向白色对象的引用前记录该引用并事后补扫。

增量更新和原始快照是两种不同逻辑下的变动识别机制,分别用于解决“对象引用关系变化”在并发标记阶段的漏标问题。它们不是数据同步层面的对比方法,而是JVM垃圾回收器(CMS vs G1)为保障三色标记正确性所采用的底层策略。掌握二者差异,关键在于理解其触发时机、作用对象和对GC行为的影响。
增量更新:拦截新增引用,把白色对象“拉回灰色”
这是CMS回收器采用的方案,核心思想是“防患于未然”——只要发现黑色对象新建立了对白色对象的引用,就立刻干预,避免该白色对象被跳过标记。
- 依赖写屏障,在每次赋值操作(如 obj.field = newObj)时触发
- 若 obj 已是黑色,而 newObj 是白色,则将 newObj 重新标记为灰色,加入待扫描队列
- 能有效堵住「黑→白」新增路径的漏标,但不处理「灰→白」引用被删除的情况
- 代价是写操作有轻微开销,尤其在高频 setter 或集合 add 场景下
原始快照(SATB):记录删除前的引用,事后补扫“曾活过的对象”
G1选择的是另一条路径:不拦新增,而是捕获“消失”。它假设所有在标记开始时存活的对象都应被保留,因此在引用被覆盖或置 null 前,先快照旧引用关系。
- 同样靠写屏障触发,但只在引用被修改/清除时生效(如 obj.field = null 或 obj.field = otherObj)
- 将原 field 指向的对象(即使已是白色)记入 satb_mark_queue,留待重新标记阶段再扫描
- 天然容忍「灰→白」引用删除导致的漏标,也无需在每次写入时介入,性能更轻量
- 副作用是可能多标一些已失效但仍被快照捕获的对象,即浮动垃圾略多
为什么不能混用或随意切换?关键约束必须守住
两种机制都建立在同一个前提上:初始标记阶段必须完成 GC Roots 的准确枚举,且之后不能再有新的 Root 动态产生。
- 例如 ThreadLocal 变量若在并发标记中被新线程首次初始化,其绑定对象可能逃逸标记范围
- 所以业务代码需避免在 GC 运行期间动态创建线程、注册监听器或初始化全局缓存
- CMS 的重新标记(Remark)阶段和 G1 的最终标记(Final Marking)阶段,都承担兜底职责,但 STW 时间长短直接受上述约束影响
实际排查漏标问题时的关注点
当遇到 NoClassDefFoundError、NullPointerException 且集中在 GC 后不久,很可能是漏标所致。此时需结合回收器类型看日志线索:
- CMS:检查是否频繁触发 Remark,或出现 “concurrent mode failure”,说明并发标记跟不上业务分配速度
- G1:关注 SATB 相关参数是否被误关闭(-XX:-G1UseSATBBarrier 是危险操作),以及 g1-log-level=finest 下的 satb_mark_queue 溢出告警
- 共性指标:观察 GC 日志中灰色对象队列是否长期积压、重新标记耗时是否陡增










