G1垃圾回收流程详解

前言

在研究垃圾回收的机制的时候,发现之前学习的G1的垃圾回收的过程有些浅,今天来带大家深入了解一下G1垃圾回收机制!

一、基于Region的分区模型

在传统分区中,是讲堆内存整体划分为年轻代与老年代

传统堆布局(CMS/Parallel GC):

┌──────────────────────────────────────┐

│ 新生代 │ 老年代 |

│ ┌─────┬─────┐ │ ┌──────────────┐ |

│ │Eden │S0/S1. │ │ │ | 连续内存 │ │

│ └─────┴─────┘ │ └──────────────┘ │

└──────────────────────────────────────┘

问题:老年代必须整体回收 → 停顿时间不可控

G1 的解决方案:逻辑分代 + 物理分区

G1 堆布局(统一管理):
┌──────────────────────────────────────┐
│ Region 0 │ Region 1 │ Region 2 │ ← Eden
├────────────┼────────────┼────────────┤
│ Region 3 │ Region 4 │ Region 5 │ ← Survivor
├────────────┼────────────┼────────────┤
│ Region 6 │ Region 7 │ Region 8 │ ← Old
├────────────┼────────────┼────────────┤
│ Region 9 │ Region 10 │ Region 11 │ ← Humongous(大对象)
└──────────────────────────────────────┘

  • Region :固定大小的内存块(默认 1~32MB,-XX:G1HeapRegionSize 控制)
  • 逻辑分代 :Region 按角色动态分配(Eden/Survivor/Old),不连续
  • 关键优势 :可选择性回收部分 Region,实现停顿时间可控

二、核心概念

在深入流程之前,必须先理解 G1 引入的几个特有概念。

1. Region(区域)

传统的 GC(如 Parallel 或 CMS)将堆物理隔断为固定的年轻代和老年代。G1 彻底打破了这一限制。

  • 它将堆内存划分为约 2048 个大小相等的独立 Region(大小在 1MB 到 32MB 之间,且为 2 的幂)。

  • 每个 Region 在逻辑上可以扮演 Eden、Survivor、Old 或 Humongous(巨型对象区)的角色。

  • 这种分而治之的策略是 G1 能够进行"增量回收"的基础。

2. Remembered Set (RSet) ------ 核心中的核心

解决的问题: 如何在回收年轻代时,不扫描整个老年代来寻找跨代引用?

  • 定义: 每个 Region 都有一个对应的 RSet。它记录了"谁引用了我这个 Region 中的对象"。

  • 本质: 是一种辅助 GC 的数据结构,存储的是从其他 Region 指向本 Region 的指针。

  • 意义: 在进行年轻代回收(Young GC)时,只需要扫描 Eden 区和对应 Region 的 RSet,就能准确找到存活对象,避免了全堆扫描。

3. Card Table (卡表)

RSet 的底层实现依赖于卡表。堆空间被划分为一个个 512 Byte 的 Card。如果一个 Card 中的对象发生了引用变化,它会被标记为 "Dirty"(脏卡)。RSet 实际上记录的就是这些脏卡的位置。

每个Region由一个个Card组成,如果Card的引用关系发生变化,就会标记成脏卡,放入脏卡队列等待处理。

4. Collection Set (CSet)

  • 定义: 在一次 GC 循环中,被选定要回收的 Region 集合。

  • 特性: Young GC 时,CSet 只包含年轻代 Region;Mixed GC 时,CSet 包含全部年轻代 Region 和部分回收价值最高的策略选出的老年代 Region。

5. SATB (Snapshot-At-The-Beginning)

其实就是原始快照,在上一篇博客我有讲

三色标记法与增量更新、原始标记

解决的问题: 并发标记阶段,用户线程修改了引用关系,导致漏标。

  • G1 通过 写前屏障(Write Barrier) 实现 SATB。

  • 在对象引用发生改变时,G1 会记录下旧的引用关系。它假设在 GC 开始那一刻存活的对象都是存活的,虽然这可能产生"浮动垃圾",但它极大地提高了并发标记的效率。

三、G1工作流程

G1 的运作过程主要分为三个阶段:Young GC并发标记周期(Concurrent Marking Cycle)Mixed GC

3.1 Young GC(年轻代回收)

  • 阶段一:根扫描 (Root Scanning)

    • 扫描静态变量、本地方法栈等常规GC Roots
  • 阶段二:更新 RSet (Update RSet)

    • 关键点: 处理"脏卡队列(Dirty Card Queue)"。

    • 在 Young GC 开始前,可能还有一些引用关系变化存在队列里没处理完。GC 线程会强制处理这些卡片,确保各个 Region 的 RSet 是最新、最准确的

  • 阶段三:扫描 RSet (Scan RSet)

    • 核心逻辑: 查找从老年代指向年轻代的引用。

    • GC 线程查看年轻代 Region 的 RSet,通过 RSet 找到老年代中哪些对象正指着自己。这些老年代对象被当作"外部根"加入扫描范围。

    • 意义: 这样就不需要扫描整个老年代,只需扫描 RSet 记录的特定 Card。

  • 阶段四:对象拷贝 (Evacuation/Object Copy)

    • 根据前两步找到的存活对象,使用复制算法,将存活对象拷贝到新的 Survivor Region 或者晋升到 Old Region。

    • G1 会记录每个 Region 的回收耗时,用于以后估算停顿时间。

  • 阶段五:处理引用 (Reference Processing)

    • 处理软引用、弱引用、虚引用等。

3.2 并发标记周期 (Concurrent Marking Cycle)

触发时机: 当整个堆的内存占用达到阈值(默认 InitiatingHeapOccupancyPercent=45%)时触发。它不是为了立刻回收内存,而是为了给 Mixed GC 做"情报收集"。

实际上就是一次三色标记,但是并没有完全进行垃圾回收。

  1. 初始标记 (Initial Mark) ------ 【STW】

    • 动作: 标记从 GC Roots 直接可达的对象。

    • 优化: 这个阶段通常"搭便车"在一次 Young GC 中完成,所以它的额外开销非常小。它会标记出每个 Region 的 Next TAMS(Top-at-Mark-Start),指明哪些对象是本次标记开始后新分配的。

  2. 根区域扫描 (Root Region Scanning) ------ 【并发】

    • 动作: 扫描刚才 Young GC 存活下来的 Survivor 区。

    • 原因: Survivor 区的对象可能引用了老年代。必须在下一次 Young GC 开始前完成,因为下一次 Young GC 会改变 Survivor 区。Survivor 区里的对象是绝对"存活"的。如果一个 Survivor 对象指向了老年代的某个对象,那么那个老年代对象也必须被标记为"存活"。

  3. 并发标记 (Concurrent Marking) ------ 【并发】

    • 动作: 从 GC Roots 开始对堆中对象进行可达性分析,递归遍历。

    • 关键技术:SATB (Snapshot-At-The-Beginning)

      • 如果此时用户程序改了引用(如 A.f = B 改为 A.f = C),写屏障会把旧的引用 B 记录下来。G1 认为"开始标记时活的对象,我都认为它是活的",防止漏标。
  4. 最终标记 (Remark) ------ 【STW】

    • 动作: 修正并发标记期间因用户程序运行而产生的引用变化。

    • 处理: 此时会处理 SATB 记录的队列,确保所有存活对象都被标记。

  5. 筛选清理 (Cleanup) ------ 【部分 STW】

    • 动作: 统计每个 Region 的回收价值(存活对象比例)。

    • 直接回收: 如果发现某个 Region 全是垃圾(存活率为 0),会直接在这个阶段清空该 Region。

    • 计算: 根据用户的停顿目标,对老年代 Region 的回收价值进行排序,生成一个 CSet (Collection Set)

四、Mixed GC

触发时机: 并发标记结束后,如果老年代的垃圾占比超过一定程度,G1 就会进入 Mixed GC 阶段。

为什么叫"混合"? 因为它在一次回收中同时处理:

  1. 所有的年轻代 Region。

  2. 部分老年代 Region(根据并发标记阶段选出的垃圾最多、回收收益最高的那些)。

Mixed GC 的执行过程与 Young GC 极其相似,你可以理解为它是"带了老年代 Region 的 Young GC":

  1. 多次增量回收:

    • G1 不会一次性把所有老年代 Region 都回收到位。为了控制停顿时间 (MaxGCPauseMillis),G1 会将选定的老年代 Region 分成好几批(默认 8 次左右,由 -XX:G1MixedGCCountTarget 控制)。

    • 每批 Mixed GC 都会回收全部年轻代 + 1/8 的选定老年代。

  2. 复制算法 (Evacuation):

    • 无论是年轻代还是老年代,存活对象都会被移动到空闲的 Region 中。

    • 好处: 移动后内存是连续的,完全消除了空间碎片问题(这是 CMS 做不到的)。

  3. RSet 更新与维护:

    • 在 Mixed GC 中,RSet 的维护更加复杂,因为它不仅要记录"老指青",还要记录"老指老"。

通过回收时间到我们设置的期望时间,就会采用回收那些回收价值高的OldRegion来进行


五、 总结:三者的逻辑链条

  1. Young GC 负责清理年轻代。

  2. 随着程序运行,老年代对象越来越多,达到 45% 阈值。

  3. 触发 并发标记,G1 开启"上帝视角"俯瞰全堆,找出哪些老年代 Region 垃圾多,哪些少。

  4. 进入 Mixed GC 阶段,分批次地回收年轻代和那些"垃圾多"的老年代 Region。

  5. 如果以上三个过程都搞不定 (比如垃圾产生速度太快),G1 会被迫触发 Full GC(单线程或并行全堆整理),这是 G1 要极力避免的噩梦。

相关推荐
滴滴答滴答答2 小时前
LeetCode Hot100 之 17 有效的括号
算法·leetcode·职场和发展
掘根2 小时前
【C++STL】二叉搜索树(BST)
数据结构·c++·算法
老鼠只爱大米2 小时前
LeetCode经典算法面试题 #20:有效的括号(数组模拟法、递归消除法等五种实现方案详细解析)
算法·leetcode··括号匹配·数组模拟法·递归消除法
yxc_inspire2 小时前
2026年寒假牛客训练赛补题(五)
算法
不想看见4042 小时前
6.3Permutations -- 回溯法--力扣101算法题解笔记
笔记·算法·leetcode
APIshop2 小时前
阿里巴巴中国站按图搜索1688商品(拍立淘)API 返回值说明
java·python·图搜索算法
前路不黑暗@2 小时前
Java项目:Java脚手架项目的通用组件的封装(五)
java·开发语言·spring boot·学习·spring cloud·bootstrap·maven
哈库纳玛塔塔2 小时前
dbVisitor 利用 queryForPairs 让键值查询一步到位
java·数据库·python
sa100272 小时前
京东评论接口调用、签名生成与异常处理
开发语言·数据库·python