G1 垃圾回收器 :SATB + TAMS核心机制深度解析

前言

在 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 线程正在遍历对象图寻找存活节点,与此同时业务线程还在不断修改对象引用。

当下面这个非常严格的时序条件同时满足时,就会出现漏标,导致存活对象被错误回收:

  1. 某对象 A 原本持有对象 B 的引用,GC 线程已经遍历完成对象 A不会再次回头扫描 A
  2. 业务线程并行执行两件事:
    ① 切断 A 对 B 的引用(A.field = null);
    ② 已经被 GC 遍历完毕的存活对象 C ,新建引用指向 BC.field = B)。

⚠️ 关键歧义提醒:

如果 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 内存分成上下两块:

  1. 地址范围 [bottom, nextTAMS)
    标记开始之前分配的老对象,需要 GC 逐个遍历、追踪引用链,判断是否存活。
  2. 地址范围 [nextTAMS, top)
    并发标记过程中新分配的对象
    👉 直接默认判定为存活,不需要 GC 遍历扫描!

2.2 为什么需要 TAMS?

并发标记运行期间,应用线程不断 new 新对象。如果所有这些对象都要参与完整的引用遍历,CPU 开销将巨大到不可接受。

SATB 规则约定:标记周期内新建的对象默认全部存活。TAMS 就是这条规则在内存管理上的落地实现------一条简单的地址分界线,用以快速区分新旧对象,省去海量扫描工作。

⚠️ 重要区分:

TAMS 只管「标记期间新建对象」,不解决上面所说的引用修改造成的漏标问题。

引用修改的漏标完全由 SATB + 写前屏障负责。

三、SATB + TAMS 完整协作时序

整个并发标记周期中,两者协调工作如下:

  1. 初始标记(Initial Mark,短暂 STW)
    • 遍历所有 Region,把每个 Region 当前的 top 地址赋值给 nextTAMS ,刷新 TAMS 分界线;
    • 此时建立了快照边界。
  2. 并发标记(Concurrent Marking)
    • 老对象 [bottom, nextTAMS) :正常遍历引用链;当旧引用被覆盖/删除时,触发写前屏障,被丢弃的对象存入 SATBMarkQueue
    • 新对象 [nextTAMS, top) :依靠 TAMS 识别,直接默认存活,无需扫描。
  3. 最终标记(Final Mark,短暂 STW)
    • 暂停所有业务线程;
    • 遍历清空 SATBMarkQueue,逐个取出队列内的对象;
    • 以每个队列对象为起点,沿着引用链完整递归标记,确保逻辑快照中的对象无一遗漏。
  4. Mixed GC
    • 根据标记结果,挑选垃圾最多的若干 Region 执行回收,既回收年轻代也回收部分老年代。

四、横向对比:SATB(G1) vs 增量更新(CMS)

对比维度 SATB(G1) 增量更新(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 并发标记的灵魂。希望本文能帮你彻底扫清这一核心知识点的障碍。

相关推荐
前端工作日常1 小时前
我学习到的JIT即时编译与机器码缓存失效
java·后端
带刺的坐椅1 小时前
Solon 的 10 种 HTTP 服务器:改一行依赖,换一个引擎
java·solon·jetty·undertow·mcp-server·htttp
xcLeigh2 小时前
Go入门:变量声明的五种方式详解
java·开发语言·golang
AI大模型-小华2 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
Mark_ZP3 小时前
【锁1】Synchronized vs ReentrantLock区别
java
Bruce_Liuxiaowei3 小时前
轻量级 IOC 管理方案:Streamlit 驱动的恶意IP情报平台
网络·jvm·网络协议·tcp/ip·安全
立心者04 小时前
SpringBoot中使用TOTP实现MFA(多因素认证)
java·spring boot·后端
枕星而眠4 小时前
C++ STL Map容器完全指南:从有序红黑树到无序哈希表
java·开发语言
逃逸线LOF4 小时前
Spring配置数据源{连接池}(Druid、c3p0)
java·数据库·spring