三色标记法用白、灰、黑三种逻辑状态将并发标记转化为可推理状态机:白色表未访问(漏标风险起点),灰色表已入队未处理完(高发干扰区),黑色表已闭环但可能引入漏标;CMS用增量更新拦截黑→白新增引用,G1用SATB捕获灰→白删除动作,二者均以容忍浮动垃圾为代价保障并发正确性。

三色标记法不是在“画颜色”,而是用白、灰、黑三种逻辑状态,把并发环境下对象存活判定的不确定性,转化成可推理、可干预的状态机。真正难理解的,是用户线程和 GC 线程同时修改引用时,灰色和黑色对象如何“失守”,导致本该活的对象被漏标——这正是中间状态干扰的核心。
白色对象:看似安全,实为风险起点
初始全白,意味着所有对象都未被检查。但问题不在“没开始”,而在“进行中变白”。比如一个对象 C 原本由灰色对象 B 引用,正等着被扫描;此时用户线程执行 B.c = null(断开),又立刻执行 A.c = C(新建立);而 A 已是黑色——它不会再被重新扫描。C 就永远卡在白色,最终被回收。
这种情况不是因为 C 没被访问,而是它被“换手”时,恰好掉出了 GC 的扫描路径。
灰色对象:GC 的工作队列,也是漏标的高发区
灰色代表“已入队、未处理完”,它是整个并发标记的推进引擎。但它的脆弱性在于:一旦 GC 暂停扫描(比如时间片用完或让出 CPU),而用户线程在此间隙修改了灰色对象的字段,就可能让新引用的对象逃过本轮标记。
常见干扰模式包括:
- 灰色对象新增指向一个白色对象(该白对象不会自动进灰队列)
- 灰色对象删除指向某个白色对象,但该白对象已被其他黑色对象悄悄接管
- 多个灰色对象交叉引用,而 GC 只按栈/队列顺序逐个处理,造成局部闭环遗漏
黑色对象:承诺“已闭环”,却成漏标温床
黑色的铁律是:它引用的所有对象都已被扫描。所以 GC 默认跳过它——这是提升并发效率的关键。但这也埋下隐患:只要黑色对象在标记期间获得新引用(比如 A.x = new X()),那个 new X() 就是白色,且永远不会被本轮扫描到。
CMS 和 G1 的应对策略差异就源于此:
- CMS 用写屏障(Write Barrier)拦截“黑色→白色”的新增引用,强制把新引用对象标灰(Incremental Update)
- G1 用写屏障捕获“灰色→白色”的删除动作,先快照旧引用(SATB:Snapshot-At-The-Beginning),确保被删对象至少在初始快照里存在,不被误判为垃圾
为什么必须容忍“浮动垃圾”?
为防止漏标而过度保守(比如反复重扫黑色对象),会严重拖慢并发速度,甚至退化为 STW。所以三色标记法主动接受一种折中:允许本轮漏掉的存活对象变成“浮动垃圾”——它仍占内存,但不会被错误回收;等下次 GC 再清理。这不是缺陷,而是对并发性与正确性权衡后的设计选择。










