写屏障通过轻量钩子实时捕获引用修改,堵住漏标漏洞:当黑色对象新增指向白色对象的引用时,立即将该白色对象标灰(增量更新)或记录旧引用(satb),确保其被重新扫描。

写屏障在增量标记阶段防止漏标记,核心是拦截用户线程对引用的修改,并主动“拉回”本该被标记但可能被跳过的存活对象。它不是靠延缓或冻结修改,而是用轻量钩子实时捕获变化,修补三色标记因并发导致的逻辑断链。
写屏障如何堵住漏标漏洞
漏标发生的典型场景是:
一个黑色对象(已扫描完)新增了对白色对象(未扫描)的引用,而该白色对象又没有其他灰色/黑色对象指向它——此时它就彻底“隐身”,会被误判为垃圾。
写屏障通过以下机制切断这个错误路径:
- 当用户线程执行类似
obj.field = whiteObj(且obj是黑色、whiteObj是白色)时,写屏障被触发 - 它不阻止赋值,但会立即将
whiteObj重新标记为灰色(SATB 方式),或把obj所在内存页记为“脏”供后续扫描(卡表方式) - 这样,即使
obj已是黑色,GC 仍会在后续阶段重新处理whiteObj,确保其可达性被确认
举个例子:A(黑)→ B(白),A 在标记后期才把 B 加进来。若无写屏障,B 就永远留在白色集合里;有了写屏障,B 被立刻推回灰色队列,后续会被扫描并标记为黑色。
增量标记中写屏障的两种主流策略
不同收集器采用不同语义的写屏障,应对漏标的方式略有差异:
-
SATB(Snapshot-At-The-Beginning)写屏障(如 G1 默认)
- 在引用被覆盖前拦截:
old_value = obj.field; obj.field = new_value - 若
old_value非空,将其加入“待重新扫描”队列(相当于保留初始快照中的所有引用) - 保证标记开始时所有可达对象都不会“消失”,新引用不影响正确性
- 在引用被覆盖前拦截:
-
增量更新(Incremental Update)写屏障(如 CMS、部分 ZGC 模式)
- 在新引用写入时拦截:
obj.field = new_value - 若
obj是黑色,且new_value是白色,则立即将new_value标灰 - 主动修补新增路径,让新建立的引用关系能被后续标记覆盖
- 在新引用写入时拦截:
两者目标一致:不让黑色对象“悄悄”拉来一个白色对象而不被察觉。
它为什么能高效工作
- 写屏障本身极轻量,通常由 JIT 编译器直接注入机器码,开销远低于全堆扫描
- 不依赖 STW,也不要求用户线程停顿,真正实现并发安全
- 和记忆集(Remembered Set)、卡表(Card Table)等结构协同,把“哪里变了”压缩成可快速定位的元数据,避免盲目遍历
没有写屏障,增量标记就只能靠频繁 STW 来冻结世界——那就不叫“增量”,而成了“伪并发”。











