三色标记法本身不引发漏标或错标,真正风险源于用户线程与GC线程并发修改引用导致“黑→白引用”约束被破坏;漏标(致命,致NPE)需黑色新增引用且灰色路径全断,错标即浮动垃圾(如栈帧销毁、新对象标黑),属可控延迟代价。
三色标记法本身不产生漏标或错标,真正引发风险的是用户线程与 GC 线程并发修改引用关系时,违反了“黑→白引用不可存在”这一核心约束。漏标会破坏程序正确性,必须拦截;错标(即多标)只导致浮动垃圾,属于可接受的延迟代价。
漏标指本该存活的对象被误判为垃圾并回收,可能引发 NullPointerException 或数据损坏。它不会随机发生,而是严格依赖以下两个同时成立的条件:
此时,白色对象既无灰色上游可被继续扫描,又被新引入的黑色对象“遮蔽”,GC 不再访问它,最终被清除。
错标实际是“多标”,即把本该回收的对象标记为存活。它不威胁程序安全,只影响内存及时释放。典型场景包括:
立即学习“Java免费学习笔记(深入)”;
objA.fieldB = null 发生在 GC 已将 B 标灰之后,B 及其子图仍会被完整扫描并标黑。所有现代并发收集器(CMS、G1、ZGC)都依赖写屏障(Write Barrier)来干预引用变更,防止漏标。不同策略应对方式不同:
objA.fieldB = null 前把 B 记入缓冲区),确保 B 不被漏掉;objC.fieldD = objB),若 objC 是黑色,就把 objB 重新推回灰色队列,补扫一次。二者目标一致——维护三色不变量,但实现时机和开销不同。SATB 更侧重“保底不丢”,增量更新更侧重“及时补救”。
浮动垃圾只是延迟回收,下一轮 GC 必然识别并清理,且不增加内存泄漏风险;漏标则直接破坏可达性语义,等价于把正在使用的对象内存归还给系统,后续访问必然出错。因此 JVM 宁可多花一点写屏障开销、多做一次重新标记(Remark),也要堵死漏标通路。这也是为什么 CMS/G1 的 Remark 阶段虽短,却必不可少。