SATB通过赋值前的写屏障捕获即将丢失的旧引用,将其压入本地队列供并发标记线程后续重新扫描,从而防止漏标;它仅响应引用断开动作,不处理数组赋值或新增引用,依赖Initial Mark的STW确保快照根集合完整。

G1 用原始快照(SATB,Snapshot-At-The-Beginning)确保并发标记不漏标,核心不是“保留变量”,而是在引用被覆盖前,捕获并记录那个即将丢失的旧对象。它不冻结数据、不复制字段值,而是在赋值动作发生的一瞬间,把旧引用“快照”下来,供后续重新扫描。
SATB 怎么抓住那个“即将消失”的引用
当应用线程执行类似 obj.field = new_obj 这样的赋值时:
- JVM 在真正写入新值之前,自动触发 pre-write barrier
- 此时
obj.field还指着旧对象(比如对象 B),这个 B 就是 SATB 要捕获的目标 - barrier 把 B 压入当前线程的 SATB 缓冲区(本地日志),后续由并发标记线程批量消费
这个机制只响应“删除”动作(旧引用断开),不关心“新增”(如 obj.field = C 中的 C)。因为新增引用天然受三色标记保护:只要 obj 是灰色(正在被扫描),它新指向的对象 C 会被立即加入扫描队列,不会漏。
为什么必须在“赋值前”捕获,而不是之后
如果等赋值完成再检查(即用 post-write barrier),就晚了:
-
obj.field已经指向 new_obj,旧对象 B 的引用彻底丢失 - 若此时 B 是唯一强引用,它可能已不可达,但尚未被标记 → 直接漏标
- SATB 的“前置性”正是为堵住这个时间窗口
漏标典型场景与 SATB 如何兜底
假设标记已推进到对象 A(黑色),A 引用着白色对象 B:
- 应用线程执行
A.field = C,B 失去最后一个强引用 - pre-write barrier 立即捕获 B,并记入 SATB 日志
- 并发标记线程稍后处理该日志,把 B 当作新根重新扫描(标灰→遍历其引用)
- B 及其可达对象因此“复活”,避免被错误回收
注意:这种复活会产生浮动垃圾——B 本该本轮回收,却因 SATB 被保留,只能等下一轮 GC 清理。
SATB 不是万能的,它有明确边界
- 它不管数组元素赋值(如
arr[i] = x),G1 对这类操作另有优化或依赖其他机制 - 它不保证所有旧引用 100% 实时入队,缓冲区满时会触发异步溢出处理,但整体仍能保障正确性
- 它和 RSet 分工明确:SATB 服务并发标记(防漏标),RSet 服务跨 Region 引用追踪(加速年轻代 GC)
本质上,SATB 是一种轻量、精准、时机关键的“引用快照协议”,靠 JIT 在字节码生成阶段硬编码 barrier,零方法调用开销。它让 G1 能在几乎不停顿的前提下,交出一份可靠的存活对象视图。

















