cms和g1采用satb而非传统三色标记,是为了在用户线程并发运行时避免漏标:satb通过初始快照冻结引用关系,写屏障仅记录被覆盖的旧引用,确保标记结果可证明安全;cms则用增量更新在写屏障中重新标记新引用。

为什么 CMS 和 G1 的并发标记要用 SATB 而不是传统三色标记?
因为并发标记时用户线程还在改对象引用,直接跑三色标记会漏标——比如一个黑色对象刚被改成指向白色对象,而这个白色对象又没被任何灰色对象重新扫描到,它就会被错误回收。SATB 用“快照”思路绕过这个问题:在标记开始前先冻结引用关系的变动视图,后续只处理那些被破坏的快照点。
关键不是“标记得快”,而是“标记结果可证明安全”。CMS 用的是增量更新(IU),G1 默认用 SATB,两者解决漏标的方向相反:CMS 在写屏障里补灰色对象的子节点,G1 在写屏障里记录被覆盖的旧引用(即“快照”)。
- SATB 不要求写屏障捕获所有写操作,只关心
obj.field = new_ref这种覆盖动作的旧值 - 必须配合初始快照(Initial Mark)阶段的 STW 完成根集合扫描,否则快照本身就不完整
- 如果应用大量修改老年代对象的字段(比如缓存热更新),
satb_mark_queue可能积压,触发额外 GC 暂停
G1 中 SATB 写屏障具体拦截什么操作?
它不拦截读、不拦截分配、不拦截方法调用,只拦截「对已存在对象字段的引用类型赋值」。比如 obj.field = otherObj,且 obj 在老年代(或属于当前收集集),这时 JVM 会在赋值前把 obj.field 原来的值(可能是 null 或某个老年代对象)压入本地 satb_mark_queue。
注意:这个写屏障由 JIT 编译器在编译期插入,不是 Java 层可配置的;也不是所有赋值都触发——年轻代对象之间的赋值通常不进 SATB 队列,除非跨代引用且满足 G1 的 remembered set 管理策略。
立即学习“Java免费学习笔记(深入)”;
- 触发条件示例:
oldObj.refField = youngObj→ 不触发(目标是年轻代);oldObj.refField = anotherOldObj→ 触发(老-老引用,旧值需记录) - 不会拦截
array[i] = obj这类数组元素赋值(G1 用另一套 card table + write barrier 处理) - 使用
-XX:+PrintGCDetails -XX:+PrintGCApplicationConcurrentTime可观察 SATB 队列溢出导致的退化暂停
SATB 标记如何与 G1 的转移(Evacuation)阶段协作?
标记阶段发现的存活对象,并不立即移动;真正转移发生在后续的混合 GC(Mixed GC)中。SATB 保证的是:在初始标记那一刻“活着”的对象,哪怕后来被用户线程断开引用,也不会被当成垃圾回收——只要它在并发标记期间被 SATB 快照链覆盖到。
但这里有个隐含前提:被 SATB 记录下来的旧引用,必须在最终标记(Remark)前完成重新扫描。否则这些“幽灵引用”会让本该回收的对象继续占着堆空间。
- 如果并发标记耗时太久,而应用持续高速修改老年代引用,
satb_mark_queue可能延迟处理,导致 Remark 阶段要扫描大量积压项,延长 STW 时间 - G1 用
-XX:G1SATBBufferSize=2048控制单个线程队列大小,太小易溢出,太大浪费内存(每个线程独占) - 没有“SATB 标记完就安全”的时刻——必须等到 Final Mark 完成,所有 SATB 队列清空、所有灰色对象扫描完毕,才能确认存活集闭合
调试 SATB 相关问题时最容易忽略的一点
很多人查 Concurrent mode failure 或 to-space exhausted,第一反应是调大堆或改 GC 线程数,但真正卡点常在应用层:比如一个定时任务每秒批量更新几千个老年代实体的关联字段,每次更新都触发 SATB 写屏障和队列入队,而这些对象本身可能早已不可达——它们只是“被快照了”,却没机会被及时清理。
这种场景下,-XX:+G1UseAdaptiveIHOP 可能反而让 G1 更早启动并发标记,加剧队列压力;更直接的干预是控制老年代对象的引用变更频次,或者用弱引用/软引用解耦生命周期。
别只盯着 GC 日志里的 SATB 字样,先确认那些被记录的旧引用,是不是真的还连着活对象。










