三色标记算法本身不解决漏标,真正防止漏标的是写屏障机制;漏标需同时满足黑色对象新增指向白色对象的引用和原有灰色路径被切断;CMS用增量更新,G1用SATB,ZGC/Shenandoah用读屏障;Unsafe操作、final字段构造器外赋值、JIT优化可能绕过屏障;写屏障是必要基础设施,不可关闭。

三色标记算法本身不“解决”漏标,它只是把并发可达性分析的问题显式建模为颜色状态;真正防止漏标的是写屏障机制与配套策略。实战中关键不是调算法,而是理解漏标触发条件、识别高风险操作、选对收集器并规避屏障绕过路径。
漏标发生的两个硬性条件必须同时成立
只有当以下两点在并发标记阶段被用户线程“精准命中”,才会导致对象被跳过:
-
黑色对象新增指向白色对象的引用:例如已标记完成的 A(黑)执行
a.field = new X(),而 X 刚分配、尚未被任何灰色对象触及(白) - 原有通向该白色对象的灰色路径被切断:比如原本由灰色对象 B 引用 X(B→X),此时 B 将该引用置为 null 或重定向,且 B 后续不再扫描 X
缺一不可。单独新增引用或单独断开引用,都不会造成漏标——前者会被写屏障捕获,后者因仍有其他路径可达,X 不会变白。
不同垃圾收集器的防御策略与实操要点
HotSpot 中主流收集器采用不同写屏障逻辑应对上述条件,选择和使用方式直接影响稳定性:
- CMS 使用增量更新(IU):赋值后检查,若发现黑→白引用,立即将源对象(如 A)重新标灰、推入标记队列。适用于老系统,但重标灰带来额外遍历开销
- G1 默认启用 SATB(原始快照):赋值前捕获旧引用值(如 B→X 即将消失),记录 X 到 SATB 缓冲区,后续在 Remark 阶段补扫。更轻量,但依赖所有赋值走屏障
- ZGC/Shenandoah 不暴露写屏障开关:通过读屏障+对象转发指针实现无暂停标记,自动覆盖绝大多数引用变更场景,无需手动干预
验证方式:启动时加 -XX:+PrintGCDetails,观察日志中是否出现 Using G1 + SATB barriers enabled 或 ConcurrentMarkSweepGeneration + Incremental Update。
高频漏标风险点与规避方法
即使启用了正确屏障,以下操作仍可能绕过 JVM 的写屏障,成为漏标“盲区”:
-
Unsafe 直接内存操作:如
Unsafe.putObject(obj, offset, newVal)完全跳过 Java 层引用管理,CMS/G1 均无法感知 - final 字段构造器外赋值:JVM 允许 final 字段在构造器中初始化时不插入屏障,若后续通过反射或 Unsafe 修改,GC 无法捕获
- JIT 优化导致的引用省略:极端情况下,逃逸分析后局部对象未实际写入堆,或标量替换使引用关系不落地,屏障无处安放
建议:并发标记高峰期避免用 Unsafe 修改对象图;final 字段务必在构造器内一次性设全;生产环境禁用 -XX:+UnlockExperimentalVMOptions 下的非常规优化。
写屏障不是可选项,是并发标记的基础设施
有人试图用 -XX:-UseCondCardMark 或类似参数关闭屏障以降开销,这是危险操作:
- 写屏障平均增加 mutator 5%~10% 开销,但换来的是标记准确性
- 一次漏标可能引发后续多次 Full GC(因对象被误删后触发 NullPointerException,进而破坏数据结构),停顿飙升至数百毫秒
- 屏障开销远低于重扫整个堆的代价,JVM 设计上将其视为必要成本,而非性能调优项
真正值得调优的是减少并发标记期的大规模引用变更,比如批量消息处理尽量延迟到 GC 间隙,或拆分长事务降低单次修改密度。










