satb机制通过在标记开始时捕获逻辑快照并配合写屏障记录引用断开,确保并发标记不漏标;g1利用其低开销、高并发特性及与remembered set协同,实现可控停顿的混合回收。

Java 中的垃圾回收器(如 G1 和 ZGC)在并发标记阶段采用 SATB(Snapshot At The Beginning)机制,来安全地处理用户线程与 GC 线程并发修改对象图时的引用变化。它不依赖“实时快照”,而是通过在标记开始时捕获一个逻辑上的对象图快照,再配合写屏障(write barrier)记录后续的引用变更,从而保证标记结果的准确性。
SATB 的核心思想:以开始时刻为基准
SATB 假设:在标记周期启动那一刻,堆中所有可达对象构成的集合是“初始快照”。GC 后续只关心这个快照中本应被标记但可能因并发修改而漏标(missed marking)的对象。它允许新创建的对象、或新建立的引用关系不参与本次标记,但必须确保“本该活的没被错回收”——即不丢失对存活对象的引用。
关键点在于:SATB 不阻止引用变更,而是把变更“记录下来”,供后续处理(如重新标记或清理阶段使用)。
写屏障捕获引用变更的时机和方式
当用户线程执行类似 obj.field = newObject 这样的赋值操作时,JVM 会在写操作前后插入写屏障逻辑(由 JIT 编译器自动注入)。G1 使用的是“pre-write barrier”(写前屏障),在字段被覆盖前触发:
- 若原字段非 null(即即将断开一个旧引用),且该对象已被标记(或处于标记中),则将该对象压入 SATB 缓存队列(每个线程本地有小缓存,满后批量刷入全局 SATB 队列);
- 这个动作本质上是“记录下:这里曾有一个指向某对象的引用,现在要被删了”,以便 GC 稍后检查该对象是否仍可通过其他路径到达;
- 注意:它不记录新引用(
newObject),只关注“断开”的旧引用——因为新引用指向的对象要么刚分配(默认视为存活,会进入下次标记),要么已存在且大概率已在初始快照中。
SATB 队列如何参与标记修正
标记主循环结束后,GC 进入“remark”(再标记)阶段,会遍历 SATB 队列中的所有对象(称为“dirty cards”或“stale references”所关联的对象),以它们为根重新扫描其引用关系:
- 例如:A → B,标记开始时 A 可达,B 被标记;之后 A.field = C,写屏障记录了 A;再标记阶段从 A 出发扫描,发现 A 此刻指向 C,于是将 C 加入标记栈;
- 这避免了因 A 放弃对 B 的引用、又未及时让 B 被其他路径覆盖,导致 B 被误回收的情况;
- 队列中的对象本身不一定“还活着”,但它们曾经持有的引用可能暴露了新的存活路径,所以必须保守扫描。
为什么 SATB 比增量更新(IU)更适合 G1 的混合收集目标
G1 关注区域化回收(Region-based)和停顿时间可控,SATB 的优势在于:
- 写屏障开销较低(只在写发生时检查原值,不需读取新值或复杂判断);
- 允许标记过程高度并发,用户线程几乎无感知;
- 再标记阶段可集中处理,便于控制暂停时间(可通过分片、增量方式处理 SATB 队列);
- 与 G1 的 Remembered Set(RSet)协作自然:SATB 保障标记完整性,RSet 保障跨 Region 引用的精确性。
不复杂但容易忽略:SATB 不是魔法,它靠的是“宁可多标、不可漏标”的保守策略,以及写屏障与再标记的紧密配合。理解它,关键是抓住“初始快照 + 变更日志 + 延迟重扫”这一闭环逻辑。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











