漏标源于并发标记时应用线程修改引用破坏三色约束,写屏障(如G1的Post-Write、ZGC的Load Barrier)是唯一保障机制;ZGC还需关注标记栈切片对齐与吞吐耦合问题。

并发标记期间应用线程改引用会导致漏标
因为标记线程和应用线程同时跑,如果应用线程在 GC 已经扫描完对象 A 后,往 A 的某个字段写入新对象 C 的引用,而 C 还没被标记过,那 C 就永远逃过了标记——它从根可达,却被当成垃圾回收。这就是典型的“漏标”(missing mark),后果是后续访问 C 时直接 NullPointerException 或内存踩踏。
三色标记法靠颜色约束保证不漏标,但必须拦截引用变更
三色标记把对象分为白(未访问)、灰(已入栈待处理)、黑(已处理完所有引用)。关键不变量是:黑色对象不能指向白色对象。一旦破坏这个约束,就可能漏标。
- 两状态(仅“已标/未标”)无法表达“已入栈但未处理完引用”的中间态,所以并发下必然出错
- 三色引入灰色作为缓冲态,让 GC 能识别“这个对象我看过,但它的引用还没扫完”
- 但应用线程仍可能在 GC 把 A 从灰变黑前,偷偷给 A 新增一个指向白色
C的引用——这就打破了黑色不连白的约束
写屏障(Write Barrier)是唯一能守住三色约束的机制
OpenJDK 各收集器(G1/ZGC/CMS)都依赖写屏障,在每次执行类似 obj.field = new_obj 这类引用写操作时插入一段检查逻辑。它不是拦截所有写,而是聚焦在“可能破坏三色约束”的写上:
-
G1用Post-Write Barrier,当发现写入目标是老年代且原引用为 null,就把新对象new_obj标记为灰色并推入 SATB(Snapshot-At-The-Beginning)缓冲区 -
ZGC用Load Barrier,在每次读取对象引用时触发,确保读到的对象至少已被标记或正在被标记;它不拦写,但通过读时补标兜底 - 漏掉写屏障或配置错误(如
-XX:+UseCondCardMark未启用)会导致 SATB 缓冲区溢出或漏记录,最终漏标
容易被忽略的工程细节:标记栈溢出和 partial array 处理
ZGC 的并发标记栈大小由 ZMarkStackSpaceLimit 控制,默认 8G,但数组对象处理不当会快速耗尽空间:
- 大数组(>
ZMarkPartialArrayMinSize,默认 4KB)不会整块入栈,而是按偏移+长度切片成多个ZMarkStackEntry - 切片要求地址对齐:
is_aligned(addr, ZMarkPartialArrayMinSize),不对齐会直接 crash - 如果应用大量创建中等大小(几 KB)的数组,又频繁修改其元素引用,写屏障 + 切片双重开销可能压垮标记吞吐
真正难调的不是参数值,而是写屏障触发频率与标记线程消费速度之间的隐式耦合——这层关系在堆 dump 和 GC 日志里根本看不到,只能靠 ZStatistics 中的 mark.stack 和 mark.barrier 指标交叉验证。


















