三色标记算法漏标的根本原因是写屏障缺失或覆盖不全。并发标记时,若对象引用更新未经写屏障捕获(如jit优化、unsafe操作、final字段构造器外赋值),已标记为黑色的对象引用新对象而灰色对象尚未扫描到,就会导致本该存活的对象被误标为白色。

三色标记算法漏标的根本原因是什么
三色标记本身是安全的,但并发扫描时,应用线程和 GC 线程同时修改对象图,就可能让本该存活的对象被误标为白色。关键不在“三色”,而在「写屏障缺失」或「屏障覆盖不全」。
- 漏标只发生在:一个对象 A 已被标记为黑色(扫描完成),它引用了新创建的对象 B,而 B 又被另一个灰色对象 C 引用 —— 但 C 还没扫描到 B,且 A 不再被重新扫描
- 典型触发场景:
obj.field = new Object()发生在并发标记阶段,且旧引用被覆盖前未通知 GC - HotSpot 中,CMS 使用增量更新(IU)写屏障,G1/ZGC 使用原始快照(SATB)写屏障,策略不同,但目标一致:捕获“即将丢失”的引用
如何验证你的 JVM 是否真的在用 SATB 或 IU
别猜,看 JVM 启动日志或运行时参数。不同垃圾收集器默认策略固定,强行改写屏障类型通常无效甚至报错。
- G1 默认启用 SATB:启动时若看到
Using G1和SATB barriers enabled就对了 - CMS 默认用 IU:日志中会出现
ConcurrentMarkSweepGeneration+Incremental Update - ZGC / Shenandoah:不暴露传统写屏障开关,它们通过读屏障+转发指针实现,
-XX:+UnlockExperimentalVMOptions下也无对应-XX:+UseXXXBarrier参数 - 错误尝试:
-XX:+UseCondCardMark是优化卡表写入的,并非切换三色策略,开了也没用
G1 的 SATB 写屏障为什么有时仍会漏标
SATB 本质是“记录引用被覆盖前的旧值”,但它依赖一个前提:所有赋值操作都必须经过屏障。而某些场景下,JVM 会绕过屏障。
- JIT 编译后内联或消除冗余写操作,导致
obj.field = null这类“清空引用”没走屏障 - 使用 Unsafe 直接内存操作(如
Unsafe.putObject)完全跳过 Java 层写屏障 - final 字段在构造器中初始化时,JVM 可能省略屏障(JMM 允许,但 GC 无法感知)
- 实操建议:避免在并发标记高峰期用
Unsafe修改对象图;final 字段尽量在构造器里一次性设完,别后续补
写屏障开销大不大?能不能关掉
不能关,也不该关。写屏障不是“可选性能开关”,而是并发可达性分析的必要基础设施。
立即学习“Java免费学习笔记(深入)”;
- 每次对象字段赋值都多一次内存写(SATB 是写入缓冲区,IU 是更新卡表),实测增加约 5%~10% 的 mutator 开销
- 但相比漏标导致的 Full GC,这点开销微不足道 —— 一次漏标可能让整个堆重扫,停顿从几毫秒飙到几百毫秒
- 常见误解:
-XX:-UseCondCardMark能关屏障?不能,它只是控制是否用条件判断优化卡表写入,底层屏障逻辑仍在 - 真正影响性能的是屏障实现质量:ZGC 把屏障逻辑压进 load 指令,G1 在 store 指令后插汇编 stub,后者更容易受 CPU 流水线干扰
三色标记的并发安全性,不取决于颜色怎么分,而取决于每一次引用变更是否被可靠捕获。很多人盯着算法图解看半天,却忘了翻一翻自己代码里那行 unsafe.putObject。










