G1 垃圾回收器
G1(Garbage First)是 JDK 9+ 的默认垃圾收集器,通过 Region 分区设计从根本上解决了 CMS 的内存碎片问题,并实现了可预测的停顿时间控制。
一、为什么需要 G1
CMS 的根本缺陷在于标记-清除算法产生内存碎片,碎片积累到一定程度,大对象无法找到连续空间,最终退化为 Serial Old 做 Full GC,停顿时间极长。
CMS 无法并发整理内存的原因:
整理 = 移动对象 + 更新所有引用指针
并发状态下移动对象:
GC 线程:正在把对象 A 从 0x1000 移到 0x2000
用户线程:同时读取 0x1000 的内容 → 拿到旧地址,对象已不在 → 程序崩溃G1 通过引入 Region 分区 + 复制算法,从根本上规避了碎片问题,同时实现了可控的停顿时间。
二、内存布局
2.1 Region 分区
G1 将整个堆划分为大量等大的 Region,打破了传统堆物理上连续的新生代与老年代分区:
传统堆:
┌──────────────────────┬─────────────────────────────────────┐
│ 新生代 │ 老年代 │
│ Eden │ S0 │ S1 │ │
└──────────────────────┴─────────────────────────────────────┘
G1 堆(每个格子是一个 Region):
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ E │ O │ S │ O │ H │ E │ │ O │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ O │ │ E │ H │ S │ O │ E │ │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ │ O │ S │ E │ O │ │ E │ O │
└────┴────┴────┴────┴────┴────┴────┴────┘
E = Eden S = Survivor O = Old
H = Humongous(大对象) 空格 = 空闲| Region 类型 | 说明 |
|---|---|
| Eden | 新对象分配区域 |
| Survivor | Minor GC 后存活对象的中转区 |
| Old | 长期存活对象晋升后的区域 |
| Humongous | 大对象专用区(对象大小 >= Region 大小的 50%),占连续多个 Region |
| 空闲 Region | 待分配,可动态变为任何角色 |
关键特性:
- 每个 Region 大小 1MB ~ 32MB,必须是 2 的幂次(由
-XX:G1HeapRegionSize指定) - Region 角色动态变化,不固定
- 逻辑上仍有分代概念,物理上不连续
2.2 Humongous Region
对象大小 >= Region 大小的 50% → 直接分配到连续的 Humongous Region
┌──────────┬──────────┬──────────┐
│ Hstart │ Hcont │ Hcont │ ← 一个大对象跨 3 个 Region
└──────────┴──────────┴──────────┘- 逻辑上属于老年代
- 若找不到连续 Region → 触发 Full GC
三、两个核心数据结构
3.1 Card Table(全局)
把整个堆按 512 字节划分为一个个 Card
Card Table 是一张全局的字节数组,每个 Card 对应一个字节
每当对象的引用字段被修改时(写屏障触发):
→ 把该对象所在的 Card 标记为 dirty
作用:感知引用关系的变动,精确到 Card 粒度3.2 RSet(每个 Region 独立维护)
RSet(Remembered Set):记录"哪些其他 Region 的哪个 Card 引用了我"
Region B 的 RSet:
┌─────────────────────────────────┐
│ Region A 的 card[3] 引用了我 │
│ Region C 的 card[7] 引用了我 │
└─────────────────────────────────┘
作用:GC 时不扫全堆,只扫 RSet 记录的 Card,精确找到跨 Region 引用3.3 两者协作关系
用户线程修改引用
│
▼
写屏障触发
│
├─ Card Table:把源对象所在 Card 标记为 dirty
│
└─ 后台 Refine 线程:持续消费 dirty Card → 更新目标 Region 的 RSetCard Table 负责感知变动,RSet 负责记录引用来源。
与 CMS 的区别:CMS 只有 Card Table,用于解决跨代引用问题;G1 在 Card Table 基础上增加了 RSet,粒度更精确,扫描范围更小。
四、Young GC
4.1 触发条件
Eden Region 全部占满,无法分配新对象时触发。
4.2 执行过程
Step 1:STW,暂停所有用户线程
Step 2:确定 CSet(Collection Set)
CSet = 所有 Eden Region + 所有 Survivor Region
Step 3:扫描 GC Roots
找到直接可达的对象
Step 4:扫描 CSet 中每个 Region 的 RSet
找到来自 Old Region 的跨 Region 引用
将这些引用对象加入 GC Roots
Step 5:复制存活对象(复制算法)
age 未达阈值 → 复制到新的 Survivor Region,age+1
age 达到阈值 → 晋升到 Old Region
Step 6:释放原 Eden 和 Survivor Region(直接变为空闲 Region,无碎片)
Step 7:恢复用户线程Young GC 前:
┌────┬────┬────┬────┬────┬────┐
│ E │ E │ E │ S │ O │ │
│(满)│(满)│(满)│age1│ │空闲│
└────┴────┴────┴────┴────┴────┘
Young GC 后:
┌────┬────┬────┬────┬────┬────┐
│ │ │ │ S │ O │ O │
│空闲│空闲│空闲│age2│ │+晋升│
└────┴────┴────┴────┴────┴────┘
原 Eden 和 S 全部释放,存活对象复制到新 S 或晋升 Old4.3 关键特点
- 全程 STW,不需要并发标记
- 通过 GC Roots + RSet 确定所有存活对象,无需扫描全堆
- 无浮动垃圾:STW 期间用户线程暂停,无新垃圾产生
- 无内存碎片:复制算法天然整理
- 当整堆占用达到 IHOP 阈值时,Young GC 会顺带完成并发标记的初始标记阶段(piggyback)
- JDK 8u40+ 起,Young GC 顺带回收 RSet 为空的 Humongous Region
五、并发标记周期
5.1 触发条件
老年代占用量 / 总堆大小 >= -XX:InitiatingHeapOccupancyPercent(默认 45%)时触发。
举例(总堆 10GB,IHOP = 45%):
老年代占用达到 4.5GB → 触发并发标记周期
注意:不是"Eden + Survivor + Old 合计占 45%"才触发
Eden 和 Survivor 的占用不计入触发条件
只有老年代的增长才会推动阈值触发目的:不直接回收对象,而是统计每个 Old Region 的垃圾占比,为后续 Mixed GC 做准备。
5.2 五个子阶段
子阶段 1:初始标记(Initial Mark)[STW,极短]
搭 Young GC 的便车(piggyback),不单独触发 STW
标记 GC Roots 直接可达的对象(只有第一层,不向下追踪)子阶段 2:根区域扫描(Root Region Scan)[并发]
扫描 Survivor Region 中指向老年代的引用
将这些引用作为并发标记的根节点
必须在下一次 Young GC 开始前完成
原因:Young GC 会改变 Survivor Region 的内容子阶段 3:并发标记(Concurrent Mark)[并发,耗时最长]
从前两阶段的结果出发,遍历整个堆的引用图
使用三色标记法标记所有存活对象
三色标记:
白色:未被标记(初始状态,最终白色 = 垃圾)
灰色:已标记,但其引用的对象还未全部扫描
黑色:已标记,且所有引用的对象都已扫描完
并发执行 → 用户线程同时修改引用关系 → 需要 SATB 写屏障保护(见第六节)子阶段 4:重新标记(Remark)[STW,较短]
处理并发标记期间 SATB 队列中剩余的对象
将队列中的白色对象重新标记为存活
为什么 STW 时间短?
并发标记线程在并发阶段已持续处理各线程本地 SATB 队列
到此阶段队列中只剩少量对象,处理量小子阶段 5:清理(Cleanup)[部分 STW,部分并发]
STW 部分:
统计每个 Region 的存活对象数量
计算每个 Region 的可回收空间
按回收价值排序(为 Mixed GC 选 Region 做准备)
释放完全空的 Region(立即回收)
并发部分(Concurrent Rebuild Remembered Sets):
为 Remark 阶段选出的 Collection Set 候选 Region 重建 RSet
(不是重置整个堆的 RSet,只针对候选回收 Region)
将完全空的 Region 加入空闲列表5.3 完整时间线
用户线程:████████░░████████████████████████░░░████████████████████████
GC 线程: ░░ ░░░░░░░░░░░░░░░░░░░░░ ░░░
│ │ │ │
初始标记 并发标记 重新标记 清理
(STW) (并发) (STW) (混合)
STW 只发生在初始标记和重新标记,停顿时间极短六、SATB(原始快照)
6.1 为什么需要 SATB
并发标记期间,用户线程与 GC 线程同时运行,可能发生漏标(把存活对象当垃圾回收):
漏标的两个必要条件(同时满足才漏标):
条件 A:黑色对象新增了对白色对象的引用
条件 B:所有灰色对象到该白色对象的路径被切断
漏标场景:
A(黑) → B(灰) → C(白) ← GC 正在标记
用户线程执行:
A.ref = C ← 条件 A:黑色对象新增对白色 C 的引用
B.ref = null ← 条件 B:灰色 B 断开了对白色 C 的路径
结果:A 是黑色不会重扫,B 扫完后 C 仍是白色 → C 被误回收!6.2 SATB 核心思想
标记开始那一刻的快照中存活的对象,本轮全部视为存活。
SATB 破坏的是条件 B:灰色对象删除引用时,把旧值记录下来,确保快照中的对象不被漏标。
6.3 写屏障实现
// 写前屏障伪代码(引用被删除 / 覆盖时触发)
void writeBarrier_pre(Object obj, Field field, Object newValue) {
Object oldValue = obj.field; // 读出旧值
if (oldValue != null) {
SATBQueue.add(oldValue); // 旧值入 SATB 队列,保证不漏标
}
obj.field = newValue; // 执行实际写入
}6.4 SATB 队列的处理
每个线程有本地 SATB 队列(减少竞争)
本地队列满 → 加入全局 SATB 队列
并发标记线程:并发标记期间持续处理各线程本地 SATB 队列
(注意:Refine 线程处理的是 dirty card queue → 更新 RSet,与 SATB 队列是两套独立机制)
重新标记阶段(STW):处理剩余未消费的 SATB 队列
取出 SATB 队列中的白色对象
→ 标记为灰色,加入扫描栈
→ 扫描其引用链,最终变为黑色
→ 保证不被漏标6.5 SATB 的代价:浮动垃圾
并发标记进行中:
标记开始时:A → B(B 存活)
用户线程:A.ref = null(B 的所有引用被删除,B 真正死亡)
SATB 记录了 B 的旧引用
结果:B 进入 SATB 队列,重新标记时被标记为存活
但实际上 B 已经死亡 → 浮动垃圾,留到下一轮 GC 回收SATB 是保守策略:宁愿多留浮动垃圾,也不误删存活对象。
6.6 SATB vs 增量更新(CMS)
| 对比项 | SATB(G1) | 增量更新(CMS) |
|---|---|---|
| 破坏条件 | 条件 B(删除引用时记录) | 条件 A(新增引用时记录) |
| 写屏障 | 写前屏障 | 写后屏障 |
| 记录内容 | 被删除引用的旧值 | 新增引用的黑色对象 |
| 重新标记 | 处理 SATB 队列,量小 | 从黑色对象重扫引用链,量大 |
| STW 时间 | 较短 | 较长 |
| 副作用 | 产生浮动垃圾 | 浮动垃圾较少 |
七、Mixed GC
7.1 触发条件
并发标记周期完成之后触发(不是堆占用达到阈值触发 Mixed GC,是触发并发标记,并发标记完成后才做 Mixed GC)。
7.2 CSet 组成
Mixed GC 的 CSet:
全部 Eden Region
全部 Survivor Region
部分 Old Region(并发标记统计出的垃圾最多的若干个)
Old Region 筛选规则:
存活率 > -XX:G1MixedGCLiveThresholdPercent(默认 85%)的跳过(复制代价高于收益)
剩余候选 Region 综合考量:可回收字节、预测疏散耗时、Region 连通性
在 MaxGCPauseMillis 时间预算内尽量多选 → Garbage First 名字的来源7.3 时间预算分配
MaxGCPauseMillis = 200ms(默认)
固定成本(必须完成,不可裁减):
Eden + Survivor 全部回收
剩余时间预算:
200ms - Eden/Survivor 回收耗时 = 剩余预算
剩余预算 → 用于 Old Region 回收
G1 基于历史统计预测每个 Old Region 的回收耗时:
预算充裕 → 多选 Old Region
预算紧张 → 少选,本轮不够下轮再回收7.4 执行过程
Step 1:确定 CSet(Eden + Survivor + 部分 Old Region)
Step 2:STW
Step 3:扫描 GC Roots
扫描 CSet 中所有 Region 的 RSet
(Old Region 的 RSet 条目更多,比 Young GC 耗时长)
Step 4:复制存活对象到空闲 Region
Eden/Survivor 存活 → 新 Survivor 或晋升 Old
Old Region 存活 → 复制到新的空闲 Old Region
Step 5:释放 CSet 中所有原 Region(无碎片)
Step 6:恢复用户线程Mixed GC 前:
┌────┬────┬────┬────┬────┬────┐
│ E │ E │ S │ O │ O │ │
│(满)│(满)│ │60% │70% │空闲│
└────┴────┴────┴────┴────┴────┘
↑被选中的 Old Region↑
Mixed GC 后:
┌────┬────┬────┬────┬────┬────┐
│ │ │ S │ │ │ O │
│空闲│空闲│新 │空闲│空闲│新 │
└────┴────┴────┴────┴────┴────┘
原 Region 全部释放,存活对象复制到右边新 Old Region7.5 连续执行多轮
并发标记完成后,Mixed GC 会连续执行多轮:
第 1 轮 → 回收一批垃圾最多的 Old Region
第 2 轮 → 再回收一批
...
退出条件(满足任一):
候选 Region 可回收字节总量 / 总堆大小 <= G1HeapWastePercent(默认 5%)
(注意:是可回收空间占总堆的比例,不是老年代垃圾比例)
已完成 G1MixedGCCountTarget(默认 8)轮
退出后 → 回到 Young GC,等待下次堆占用达到 45%7.6 与 Young GC 的对比
| 对比项 | Young GC | Mixed GC |
|---|---|---|
| CSet 组成 | Eden + Survivor | Eden + Survivor + 部分 Old |
| 触发方式 | Eden 满,被动触发 | 并发标记后,主动触发 |
| RSet 扫描 | 量少(只扫新生代 RSet) | 量多(还要扫 Old 的 RSet) |
| 停顿时间 | 较短 | 较长 |
| 执行流程 | 完全一样 | 完全一样 |
| 本质区别 | — | CSet 多了 Old Region |
八、Full GC(退化)
G1 正常运行时不做 Full GC,以下场景会退化:
| 触发场景 | 说明 |
|---|---|
| 疏散失败 | 复制存活对象时,找不到空闲 Region |
| 并发标记跟不上分配 | 老年代增长速度过快,并发标记还未完成老年代就将满 |
| Humongous 分配失败 | 大对象找不到足够连续的 Region |
Full GC 行为:
JDK 8:Single 线程 Serial 方式,对整堆标记整理,可能停顿极长
JDK 10+:改为多线程并行 Full GC,停顿时间大幅改善避免 Full GC 的关键:保证堆中始终有足够的空闲 Region(-XX:G1ReservePercent)。
九、Humongous Region 的回收
关键前提:Humongous 对象不参与正常的疏散(evacuation),不会被复制到新 Region。 G1 只对其做判活(liveness check),确认死亡后直接原地释放整个 Region。 只有 Full GC 作为最后手段时才可能移动 Humongous 对象。
| 回收时机 | 触发条件 | 说明 |
|---|---|---|
| Eager Reclaim(Young GC 期间) | 无强代码根(code root)且 RSet 条目稀少或为空,JDK 8u60+ | 判活后原地释放,不复制,是最及时的路径 |
| 并发标记 Cleanup 阶段 | 并发标记后确认不可达 | Cleanup STW 阶段回收已死亡的 Humongous Region |
| Full GC | 以上路径均未回收时的兜底 | 最后手段,可能移动对象 |
注意:Humongous Region 不会像普通 Old Region 一样被选入 Mixed GC 的 CSet 参与复制回收。
十、完整运行节奏
对象分配
│
▼
Eden Region 满
│
▼
Young GC(STW)
│
├─ 存活对象复制 → 新 Survivor 或晋升 Old
├─ 堆占用达到 45%?→ 顺带做初始标记(piggyback)
│
▼
┌──────────────────────────────────────────────┐
│ 并发标记周期(与用户线程并发) │
│ 根区域扫描 → 并发标记(SATB) → 重新标记 → 清理│
└──────────────────────┬───────────────────────┘
│ 完成,知道每个 Old Region 价值
▼
Mixed GC → Mixed GC → Mixed GC(老年代清理达到目标)
│
▼
回到 Young GC
异常路径:
疏散失败 / 老年代撑满 / Humongous 分配失败
│
▼
Full GC(尽量避免)十一、G1 vs CMS
| 对比项 | CMS | G1 |
|---|---|---|
| 内存布局 | 连续新生代 + 老年代 | 分散的 Region |
| 回收算法 | 标记-清除(有碎片) | 复制算法(无碎片) |
| 停顿控制 | 无法控制 | MaxGCPauseMillis 软目标 |
| 跨代引用 | Card Table | Card Table + RSet(更精确) |
| 漏标解决 | 增量更新(写后屏障) | SATB(写前屏障) |
| Full GC | Serial Old 单线程(极慢) | JDK 10+ 多线程并行 |
| 浮动垃圾 | 并发清除阶段产生 | 并发标记阶段产生(SATB) |
| 适用堆大小 | 几 GB ~ 几十 GB | 几 GB ~ 上百 GB |
| JDK 默认 | JDK 8 非默认 | JDK 9+ 默认 |
| JDK 状态 | JDK 9 废弃,JDK 14 移除 | 主流,持续优化 |
十二、常用参数
12.1 核心参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-XX:+UseG1GC | — | 启用 G1(JDK 9+ 默认) |
-XX:MaxGCPauseMillis | 200ms | 停顿时间目标(软目标,非硬保证) |
-XX:InitiatingHeapOccupancyPercent | 45% | 触发并发标记的阈值:老年代占用量 / 总堆大小达到此比例时触发 |
-XX:G1HeapRegionSize | 自动 | Region 大小(1~32MB,必须是 2 的幂次) |
12.2 调优参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-XX:G1MixedGCLiveThresholdPercent | 85% | Old Region 存活率超过此值则跳过不回收 |
-XX:G1HeapWastePercent | 5% | 老年代垃圾低于此比例时停止 Mixed GC |
-XX:G1MixedGCCountTarget | 8 | 每轮并发标记后最多执行 Mixed GC 的次数 |
-XX:G1ReservePercent | 10% | 预留堆空间比例,防止疏散失败 |
-XX:ConcGCThreads | 自动 | 并发标记线程数(建议 = ParallelGCThreads / 4) |
12.3 参数示例
# 典型 Web 服务配置(8GB 堆)
-XX:+UseG1GC
-Xms8g -Xmx8g
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=40
-XX:G1HeapRegionSize=16m
-XX:G1ReservePercent=15
-XX:ConcGCThreads=4十三、面试高频问题
| 问题 | 答案要点 |
|---|---|
| G1 为什么不产生内存碎片? | 使用复制算法,存活对象复制到新 Region,原 Region 整体释放 |
| G1 如何控制停顿时间? | 建立预测模型,动态调整 CSet 中 Old Region 数量,在时间预算内尽量多回收 |
| 什么是 Mixed GC? | Young GC + 部分 Old Region 的回收,CSet 多了垃圾最多的若干 Old Region |
| G1 的并发标记用什么解决漏标? | SATB(写前屏障),删除引用时记录旧值,重新标记时统一处理 |
| SATB 和增量更新有什么区别? | SATB 破坏漏标条件 B(删除路径),增量更新破坏条件 A(新增引用),SATB STW 更短 |
| G1 的 RSet 是什么? | 每个 Region 独立维护,记录哪些其他 Region 引用了自己,GC 时不用扫全堆 |
| Humongous 对象何时回收? | Young GC 顺带(RSet 为空时)、并发标记清理阶段、Mixed GC |
| G1 什么时候会触发 Full GC? | 疏散失败(无空闲 Region)、并发标记跟不上分配速度、Humongous 分配失败 |
| G1 并发标记周期触发条件是什么? | 老年代占用量 / 总堆大小 >= IHOP(默认 45%),不是整堆占用率 |
| G1 和 CMS 的核心区别是什么? | Region 分区 + 复制算法解决碎片,可控停顿时间,SATB 解决漏标 |
