前言
在 JVM 垃圾回收领域,G1(Garbage First) 凭借其可预测的停顿时间和优秀的吞吐表现,已经成为服务端应用的首选收集器之一。而支撑 G1 正确进行并发标记的核心,就是 SATB(Snapshot At The Beginning) 算法以及 TAMS(Top At Mark Start) 指针机制。
一、SATB:Snapshot At The Beginning(原始快照)
1.1 核心思想
一轮并发标记刚刚启动的瞬间,G1 会形成一张逻辑快照:凡是快照中存在的对象,本轮 GC 都必须当作存活对象处理。
通俗理解:在标记启动那一刻还活着的对象,哪怕在标记进行中彻底没人使用了,本轮 GC 也不能回收它,只能等到下一轮 GC 清理。由此产生的“可回收但本轮不回收”的对象,被称为浮动垃圾。
1.2 并发标记存在的风险(附完整时序案例)
GC 线程正在遍历对象图寻找存活节点,与此同时业务线程还在不断修改对象引用。当下面这个非常严格的时序条件同时满足时,就会出现漏标,导致存活对象被错误回收:
⚠️ 关键歧义提醒:如果 C 还没有被 GC 遍历到,那么后续 GC 扫描到 C 时自然能发现 B,完全不会产生漏标!漏标成立的硬性前提是:创建新引用的对象 C,GC 已经扫描完毕,不会再被访问。
从客观事实来看:B 依然被 C 引用,B 是存活对象。从 GC 视角来看:A 已经处理完毕不再回看;C 早已扫描完成,GC 看不到 C 新建的引用。GC 误以为 B 无任何引用,判定为垃圾,一旦回收就会触发程序崩溃。
SATB 如何拦截这个场景(完整处理流程)
当业务线程执行 A.field = null,切断 A→B 时:赋值前,写前屏障(Pre-Write Barrier)触发,把被丢弃的旧引用对象 B 存入 SATBMarkQueue。
重点:屏障捕获的是被覆盖消失的对象 B,而不是新增引用的目标。
存入队列 不等于立刻扫描:
- 并发标记阶段,GC 不会实时处理 SATB 队列,而是继续正常遍历堆;
- 等到 Final Mark(最终标记,短暂 STW 阶段):- GC 暂停所有业务线程;- 遍历整个 SATBMarkQueue,取出队列里所有对象(案例中的 B);- 以这些对象为起点,沿着引用链完整递归扫描、标记整条引用链上所有对象,最终保证 B 以及 B 关联的所有对象都被标记为存活,本轮 GC 绝不会回收。
对比区分(重点修正,清晰区分新增 / 删除两种行为)- CMS 增量更新:只监控新增产生的引用;不处理单纯的引用删除。- G1 SATB:只监控被删除、被覆盖的旧引用;新增引用本身不需要屏障处理。
补充关键区分:单纯新增引用不会引发漏标;单纯删除引用也不会引发漏标;只有「旧引用断开 + 新引用来自已经扫描完成的对象」组合在一起,才会触发漏标。SATB 通过在删除引用侧做拦截,完美打破了该组合条件。
二、TAMS:Top At Mark Start(标记启动时的分配顶端指针)
SATB 解决了老对象引用变更的漏标问题,但对于标记过程中新创建的对象,需要另一套高效机制来应对,这就是 TAMS。
2.1 TAMS 数据结构
每一个 Region 内部维护两个指针,标记阶段启动时记录当时的内存分配上限。指针顺序如下:
bottom(Region 起始地址) → prevTAMS → nextTAMS → top(当前已分配位置) → end(Region 末尾)并发标记【刚启动一瞬间】nextTAMS = top,两者地址完全重合;
但是标记开始之后,应用线程不断分配对象,top 持续向右移动,nextTAMS 固定不动!
- nextTAMS(NTAMS):本轮并发标记启动瞬间,记录该 Region 当时的 top 地址;
- prevTAMS:上一轮并发标记记录的 nextTAMS,用于前后轮标记区间判断。
TAMS 把 Region 内存分成上下两块:
2.2 为什么需要 TAMS?
并发标记运行期间,应用线程不断 new 新对象。如果所有这些对象都要参与完整的引用遍历,CPU 开销将巨大到不可接受。
SATB 规则约定:标记周期内新建的对象默认全部存活。TAMS 就是这条规则在内存管理上的落地实现——一条简单的地址分界线,用以快速区分新旧对象,省去海量扫描工作。
⚠️ 重要区分:TAMS 只管「标记期间新建对象」,不解决上面所说的引用修改造成的漏标问题。引用修改的漏标完全由 SATB + 写前屏障负责。
三、SATB + TAMS 完整协作时序
整个并发标记周期中,两者协调工作如下:
四、横向对比:SATB(G1) vs 增量更新(CMS)
| 监控行为 | 旧引用被删除、覆盖 | 程序新建引用关系 |
| 使用屏障 | 写前屏障(Pre-Write Barrier) | 写后屏障(Post-Write Barrier) |
| 处理方式 | 被丢弃对象存入 SATB 队列;最终标记 STW 阶段沿引用链统一扫描标记 | 把产生新引用的源对象重新加入标记队列,再次遍历 |
| 产生结果 | 产生浮动垃圾(本轮不回收) | 无浮动垃圾(重新标记可修正) |
| 配套组件 | TAMS 区分标记期间新建对象 | 不存在 TAMS(新生代由 Young GC 负责) |
五、面试精简背诵版
SATB 是 G1 并发标记采用的算法。并发标记启动瞬间生成逻辑快照,快照内存在的对象本轮视作存活;当旧引用被覆盖/删除,写前屏障捕获被丢弃的引用存入 SATBMarkQueue,等到最终标记阶段 STW,遍历队列,以队列对象为起点沿着引用链完整标记,防止存活对象被错误回收,代价是产生浮动垃圾。
TAMS 是每个 Region 内部的地址指针,初始标记阶段记录标记启动时刻的分配位置;TAMS 分界线之后是标记周期中新创建的对象,直接默认存活,不用遍历标记。
协作关系:SATB 负责处理老对象引用变更的漏标问题;TAMS 用来区分标记期间新创建对象;二者配合,保障并发标记的正确性。
六、两个深度追问
追问一:为什么 SATB 不能区分“临时断开”与“永久删除”?
你可能会想:既然对象 B 的引用断开了,为什么 SATB 不进一步判断 B 是否真的变成了垃圾?
原因在于并发场景下无法零成本地精确区分。引用断开的那一刻,JVM 根本无法预知未来 0.1ms 内用户线程是否会重新建立引用。
- 若选择“冒进回收”,一旦引用恢复就会导致 JVM 崩溃;
- 若选择“实时扫描全堆判断”,则会严重拖慢并发标记进度,停顿时间不可控。
因此 SATB 采用保守策略——宁可放过,不可错杀。这是并发 GC 中典型的性能与安全权衡。
追问二:CMS 为什么没有 TAMS 这样的机制?
CMS 没有类似 TAMS 的机制,根本原因在于分代设计差异:
- CMS 仅负责老年代的回收,并发标记时新创建的对象全部分配在新生代(Young Gen);
- 新生代对象完全由独立的 Young GC 负责,CMS 根本不用插手,自然不需要在 Region 内划分标记边界。
而 G1 的 Region 可以灵活划作新生代或老年代,新分配的对象可能直接进入正在并发标记的 Region 中,因此必须有 TAMS 这种机制来划清界限,默认存活,避免大量无效扫描。
总结
- SATB 以“逻辑快照”为核心,通过在删除引用侧记录旧对象,保障并发安全;
- TAMS 用一条简单的地址指针,将标记期间的新对象默认标记为存活,极大降低扫描开销;
- 两者一个解决“引用变更漏标”,一个解决“新对象识别”,共同支撑起 G1 高效、正确的并发标记。
理解 SATB + TAMS,就抓住了 G1 并发标记的灵魂。希望本文能帮你彻底扫清这一核心知识点的障碍。
网硕互联帮助中心



评论前必须登录!
注册