增量更新通过写屏障捕获黑色对象新增白色引用,将其重新标记为灰色,确保后续遍历;CMS采用该策略,G1则用SATB快照记录引用删除以避免漏标。

增量标记时对象引用被修改会导致漏标
并发标记阶段,应用线程和 GC 线程同时运行。如果一个对象 A 正在被标记为灰色(已入队但子对象未扫描),此时应用线程执行 A.field = B,把 A 原来指向的旧对象 C 替换为新对象 B,而 B 又恰好是白色(未访问),就可能永远不被标记——因为 A 已经扫过一遍子引用,不会再重新检查它。这就是典型的“漏标”问题。
Write Barrier 的核心作用是捕获写操作并补救
Write Barrier 不是阻止写,而是拦截赋值动作,在关键节点插入逻辑来保活或重访。不同收集器策略不同:
-
SATB(G1):用 pre-write barrier,在
A.field = B执行前,把旧引用C记录进 SATB 缓冲区。后续并发标记会确保 C 仍被视作存活(哪怕它实际已不可达),等下次 GC 再清理——牺牲一点空间换一致性。 -
Logging(CMS/G1 某些模式):用 post-write barrier,在赋值后记录
A → B这条新引用,GC 扫描时把 B 加入灰色队列重新处理。 - ZGC / Shenandoah 的读屏障替代方案:不依赖 write barrier 补漏,而是靠读屏障在每次读取对象时动态判断是否需重映射或标记——但这对 write barrier 的依赖大幅降低。
写屏障实现必须满足两个硬约束
否则再精巧的算法也会失效:
- 所有跨代/跨 region 的引用更新都必须经过 write barrier,不能有绕过路径(比如 JNI 直接内存写、Unsafe.putObject 未 hook);
- write barrier 自身不能触发 GC 或分配堆内存,否则会陷入递归调用死循环——所以 G1 的 SATB barrier 只做原子写入缓冲区,ZGC 的着色指针更新也只改位,不 malloc。
调试时最容易忽略的是 barrier 的生效范围
比如你在 JVM 中启用了 -XX:+UseG1GC,但没确认 -XX:+UnlockExperimentalVMOptions 是否必要(某些老版本 G1 的 barrier 优化需显式解锁),或者误以为 CMS 的 -XX:+CMSIncrementalMode 自动启用 logging barrier(其实它默认关闭,得配 -XX:+CMSScavengeBeforeRemark 等配合项)。更隐蔽的是:JIT 编译后可能内联掉某些 barrier 插桩,导致部分热点路径漏保护——这时得用 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 观察 remark 阶段是否频繁 fallback 到 STW。

















