
前言
在研究垃圾回收的机制的时候,发现之前学习的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 做"情报收集"。
实际上就是一次三色标记,但是并没有完全进行垃圾回收。
-
初始标记 (Initial Mark) ------ 【STW】
-
动作: 标记从 GC Roots 直接可达的对象。
-
优化: 这个阶段通常"搭便车"在一次 Young GC 中完成,所以它的额外开销非常小。它会标记出每个 Region 的
Next TAMS(Top-at-Mark-Start),指明哪些对象是本次标记开始后新分配的。
-
-
根区域扫描 (Root Region Scanning) ------ 【并发】
-
动作: 扫描刚才 Young GC 存活下来的 Survivor 区。
-
原因: Survivor 区的对象可能引用了老年代。必须在下一次 Young GC 开始前完成,因为下一次 Young GC 会改变 Survivor 区。Survivor 区里的对象是绝对"存活"的。如果一个 Survivor 对象指向了老年代的某个对象,那么那个老年代对象也必须被标记为"存活"。
-
-
并发标记 (Concurrent Marking) ------ 【并发】
-
动作: 从 GC Roots 开始对堆中对象进行可达性分析,递归遍历。
-
关键技术:SATB (Snapshot-At-The-Beginning)。
- 如果此时用户程序改了引用(如
A.f = B改为A.f = C),写屏障会把旧的引用B记录下来。G1 认为"开始标记时活的对象,我都认为它是活的",防止漏标。
- 如果此时用户程序改了引用(如
-
-
最终标记 (Remark) ------ 【STW】
-
动作: 修正并发标记期间因用户程序运行而产生的引用变化。
-
处理: 此时会处理 SATB 记录的队列,确保所有存活对象都被标记。
-
-
筛选清理 (Cleanup) ------ 【部分 STW】
-
动作: 统计每个 Region 的回收价值(存活对象比例)。
-
直接回收: 如果发现某个 Region 全是垃圾(存活率为 0),会直接在这个阶段清空该 Region。
-
计算: 根据用户的停顿目标,对老年代 Region 的回收价值进行排序,生成一个 CSet (Collection Set)。
-
四、Mixed GC
触发时机: 并发标记结束后,如果老年代的垃圾占比超过一定程度,G1 就会进入 Mixed GC 阶段。
为什么叫"混合"? 因为它在一次回收中同时处理:
-
所有的年轻代 Region。
-
部分老年代 Region(根据并发标记阶段选出的垃圾最多、回收收益最高的那些)。
Mixed GC 的执行过程与 Young GC 极其相似,你可以理解为它是"带了老年代 Region 的 Young GC":
-
多次增量回收:
-
G1 不会一次性把所有老年代 Region 都回收到位。为了控制停顿时间 (MaxGCPauseMillis),G1 会将选定的老年代 Region 分成好几批(默认 8 次左右,由
-XX:G1MixedGCCountTarget控制)。 -
每批 Mixed GC 都会回收全部年轻代 + 1/8 的选定老年代。
-
-
复制算法 (Evacuation):
-
无论是年轻代还是老年代,存活对象都会被移动到空闲的 Region 中。
-
好处: 移动后内存是连续的,完全消除了空间碎片问题(这是 CMS 做不到的)。
-
-
RSet 更新与维护:
- 在 Mixed GC 中,RSet 的维护更加复杂,因为它不仅要记录"老指青",还要记录"老指老"。
通过回收时间到我们设置的期望时间,就会采用回收那些回收价值高的OldRegion来进行
五、 总结:三者的逻辑链条
-
Young GC 负责清理年轻代。
-
随着程序运行,老年代对象越来越多,达到 45% 阈值。
-
触发 并发标记,G1 开启"上帝视角"俯瞰全堆,找出哪些老年代 Region 垃圾多,哪些少。
-
进入 Mixed GC 阶段,分批次地回收年轻代和那些"垃圾多"的老年代 Region。
-
如果以上三个过程都搞不定 (比如垃圾产生速度太快),G1 会被迫触发 Full GC(单线程或并行全堆整理),这是 G1 要极力避免的噩梦。
