写屏障通过在老年代对象字段赋值指向新生代对象时触发增量更新,将其加入重扫队列,确保新增跨代引用被标记,从而防止漏标;它配合卡表与本地缓冲区实现高效、低开销的并发标记保障。

Java 垃圾回收中,写屏障是实现增量更新、防止漏标的核心机制。它在对象引用字段被修改时插入一段轻量级代码,捕获跨代或跨区域的引用变化,确保 GC 在并发标记过程中不遗漏本该存活的对象。
写屏障如何捕获“新老对象间”的引用写入
当应用线程修改一个老年代对象(如 OldObject)的字段,使其指向一个新生代对象(如 NewObject),这个动作可能让原本不可达的 NewObject 变成可达——但此时 GC 线程可能已标记完 OldObject,若不干预就会漏标。写屏障在此刻介入:只要检测到“老对象 → 新对象”的写操作,就将 OldObject 加入 增量更新队列(Incremental Update Buffer),后续重新扫描它,检查是否有新增的跨代引用。
- 典型触发场景:oldObj.field = newObj;
- JVM(如 G1、ZGC)会在字节码解释或 JIT 编译时,在赋值指令前后自动插入写屏障逻辑
- 写屏障本身不阻塞应用线程,只做一次指针判断和队列追加(通常用线程本地缓冲区避免竞争)
增量更新 vs 原始快照(SATB):为什么选前者防漏标
增量更新(IU)与原始快照(SATB)是两种主流写屏障策略。IU 主动“补救”新增引用,而 SATB 侧重“冻结旧关系”。对防范漏标而言,IU 更直接:
- IU 在引用建立时记录“源头”,后续重扫该源头,确保新引用被标记
- SATB 在引用断开前记录“旧目标”,用于保留已标记但即将失联的对象,主要防多标(mis-marking),而非漏标
- 漏标本质是“新引用未被发现”,IU 正是对症下药;G1 默认使用 SATB,但在并发标记阶段配合 IU 式的“重新标记”步骤来兜底漏标风险
实际落地中的关键设计细节
单纯插入写屏障不够,还需配套机制才能高效防漏标:
- 缓冲区聚合:每个线程维护本地写屏障缓冲区(如 G1 的 Dirty Card Queue),攒一批再批量提交到全局队列,减少同步开销
- 延迟重扫描:不是每次写都立刻重扫,而是在并发标记的“重新标记”(Remark)阶段或并发清理前集中处理增量队列
- 卡表(Card Table)协同:写屏障常与卡表结合——当老年代对象被修改,对应卡页被标记为 dirty,后续仅扫描 dirty 卡内的对象,缩小重扫范围
- 屏障类型选择:G1 使用 post-write barrier(写后屏障),ZGC 使用 load barrier + colored pointer,但核心目标一致:低成本捕获引用变更
一个漏标发生的典型链路与 IU 如何切断它
假设标记进行中,GC 已扫描完 A 对象,A 指向 B;随后应用线程执行 A.field = C(C 是新分配且尚未标记的对象)。若无写屏障:
- A 不再被扫描 → C 永远不会被发现 → 漏标发生
- 有了 IU 写屏障:A 被加入增量队列 → 后续重扫 A → 发现 A→C → 标记 C
- 即使 C 本身又引用了 D,只要 D 在 C 被标记前未被其他路径访问,仍需靠 C 的后续扫描传播,但漏标链已在第一环被截断
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











