G1:Region 化收集器,JDK 9+ 的默认选择
当 CMS 在内存碎片和 Full GC 的泥潭中挣扎时,G1 横空出世。
它用一个革命性的设计——把堆划分为 Region——解决了 CMS 的所有痛点,并成为 JDK 9+ 的默认垃圾收集器。
G1 的核心思想:Region 化
传统分代 vs G1 Region
┌─────────────────────────────────────────────────────────────┐
│ 传统分代收集器 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 堆 │ │
│ │ ┌───────────────┬───────────────────────────┐ │ │
│ │ │ 年轻代 │ 老年代 │ │ │
│ │ │ ┌───┬───┬───┐ │ │ │ │
│ │ │ │Eden│S0 │S1 │ │ │ │ │
│ │ │ └───┴───┴───┘ │ │ │ │
│ │ └───────────────┴───────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 特点: Eden、S0、S1、老年代,大小固定,不可调整 │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ G1 收集器 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 堆 │ │
│ │ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │
│ │ │ Eden│ Eden│ S │ Old │ Old │ Old │ Hum │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │ │
│ │ │ Eden│ S │ Old │ Old │ Hum │ Hum │ Eden│ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │ │
│ │ │ Old │ Old │ Eden│ S │ Old │ Eden│ S │ │ │
│ │ └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 特点:堆被划分为多个 Region,每个 Region 可以是 Eden/Survivor/Old │
│ │
└─────────────────────────────────────────────────────────────┘Region 的设计
java
// G1 的 Region 大小配置
public class G1RegionSize {
// 默认:-XX:G1HeapRegionSize=1~32MB(自动计算)
// 不能太小:太小会导致 RSet 膨胀
// 不能太大:太大影响回收精度
// Region 数量 = 堆大小 / Region 大小
// 4GB 堆 / 2MB Region = 2048 个 Region
}特殊 Region
| 类型 | 说明 | 特点 |
|---|---|---|
| Humongous | 大对象(超过 50% Region 大小) | 连续多个 Region,优先回收 |
| Old Region | 老年代对象 | 组成老年代 |
| Survivor Region | 存活对象 | S0/S1 角色 |
| Eden Region | 新生代对象 | 组成年轻代 |
G1 的回收过程
Young GC:年轻代收集
┌─────────────────────────────────────────────────────────────┐
│ Young GC 过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 选定所有年轻代 Region,Stop The World │
│ ┌─────────────────────────────────────────────────┐ │
│ │ [E][E][E][E][E][S][S] │ [Old][Old][Old][Hum] │ │
│ │ 年轻代 Regions │ 其他 Regions │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 2. 复制算法:将存活对象复制到 Survivor 或晋升到 Old │
│ ┌─────────────────────────────────────────────────┐ │
│ │ [E][E][E][E] │ [S] │ [Old][Old][Old][Hum] │ │
│ │ 新 Eden │存活 │ 其他 Regions │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 3. 清理空 Region │
│ │
└─────────────────────────────────────────────────────────────┘Mixed GC:混合收集
┌─────────────────────────────────────────────────────────────┐
│ Mixed GC 过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 触发条件:老年代使用率超过阈值 │
│ -XX:InitiatingHeapOccupancyPercent=45(默认) │
│ │
│ 1. 初始标记(STW):标记 GC Roots │
│ - 年轻代收集时同时进行 │
│ │
│ 2. 并发标记:遍历对象图,不 STW │
│ │
│ 3. 最终标记(STW):修正并发变化 │
│ │
│ 4. 筛选回收(STW): │
│ - 根据停顿时间目标,选择回收价值最高的 Region │
│ - 回收顺序:垃圾最多的 Region → 垃圾次多的 → ... │
│ │
│ 5. 复制整理:将存活对象复制到空 Region │
│ - 同时完成 compaction(无碎片!) │
│ │
└─────────────────────────────────────────────────────────────┘G1 的核心优势
优势 1:可预测的停顿时间
bash
# 设置停顿时间目标
java -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \ # 希望停顿不超过 200ms
your.Application
# G1 会动态调整:
# - 每次收集的 Region 数量
# - 优先收集垃圾最多的 Region
# - 牺牲部分吞吐量,换取可控停顿优势 2:无内存碎片
CMS vs G1 整理效果对比
CMS(标记-清除):
┌─────────────────────────────────────────────────────────────┐
│ [██][ ][██][ ][██][ ][██][ ][██][██] │
│ 碎片,无法分配大对象 │
└─────────────────────────────────────────────────────────────┘
G1(复制整理):
┌─────────────────────────────────────────────────────────────┐
│ [██][██][██][██][██] [空闲][空闲][空闲] │
│ 连续空间,可分配大对象 │
└─────────────────────────────────────────────────────────────┘优势 3:内存布局灵活
java
// G1 可以动态调整各代大小
// CMS:Eden : Survivor : Old 比例固定
// G1:Region 数量和角色动态变化Remembered Set(RSet)
为什么需要 RSet?
年轻代对象可能被老年代 Region 引用,扫描整个老年代太慢。
┌─────────────────────────────────────────────────────────────┐
│ 跨 Region 引用问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Region A (Old) ──引用──→ Region B (Eden) │
│ │
│ 问题:回收 Eden 时,如何发现 Region B 中的对象? │
│ 方案:Region A 记录对 Region B 的引用 │
│ 工具:Remembered Set(RSet) │
│ │
└─────────────────────────────────────────────────────────────┘RSet 实现原理
java
// RSet 结构
public class RSet {
// 每个 Region 都有一个 RSet
// RSet 记录:谁引用了我
// 其他 Region 对本 Region 对象的引用
// Card Table 的进化
// 1. Card Table:将老年代划分为多个 Card(通常 512 字节)
// 2. RSet:每个 Card 对应一个 bit set,记录引用关系
}RSet 的开销
| 堆大小 | Region 大小 | Region 数量 | RSet 开销 |
|---|---|---|---|
| 4GB | 2MB | 2048 | ~1% |
| 32GB | 4MB | 8192 | ~2% |
| 64GB | 8MB | 8192 | ~2% |
RSet 是 G1 实现可预测停顿的代价。
核心参数配置
bash
# 启用 G1
java -XX:+UseG1GC your.Application
# 停顿时间目标(最重要)
-XX:MaxGCPauseMillis=200
# Region 大小
-XX:G1HeapRegionSize=2m
# 触发 Mixed GC 的老年代阈值
-XX:InitiatingHeapOccupancyPercent=45
# Mixed GC 回收的老年代 Region 比例
-XX:G1HeapWastePercent=5
# 每次 Mixed GC 最多回收的 Region 数
-XX:G1MixedGCCountTarget=8
# JDK 11+ 推荐配置
java -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=4m \
-XX:InitiatingHeapOccupancyPercent=45 \
-Xmx4g -Xms4g \
your.ApplicationG1 调优经验
问题 1:频繁 Young GC
bash
# 症状:YGC 次数过多
# 原因:Eden Region 太小或对象分配太快
# 解决:增大年轻代
-XX:G1NewSizePercent=10 # 年轻代最小比例
-XX:G1MaxNewSizePercent=60 # 年轻代最大比例问题 2:Mixed GC 时间过长
bash
# 症状:Mixed GC 停顿时间超过 MaxGCPauseMillis
# 原因:每次回收的 Region 太多
# 解决:减少每次 Mixed GC 的 Region 数
-XX:G1MixedGCCountTarget=16 # 增加回收次数
-XX:G1HeapWastePercent=10 # 允许更多垃圾问题 3:Humongous 对象过多
bash
# 症状:大对象过多,导致频繁 Full GC
# 原因:对象超过 Region 大小的 50%
# 解决:增大 Region 大小,或优化代码减少大对象
-XX:G1HeapRegionSize=4mG1 日志解读
text
# Young GC 日志
2024-01-15T10:30:00.123: [GC pause (G1 Evacuation Pause) (young)
# 年轻代回收
# Evacuation Pause:复制阶段
Using 8 workers
# 8 个 GC 线程
2024-01-15T10:30:00.123: [GC pause (G1 Evacuation Pause) (young)
# 开始
(Eden: 1320.0M(1320.0M)->0.0B(1180.0M)
# Eden: 使用 1.3GB → 回收后 0,使用量 0 → 回收后 Eden 变成 1.18GB
Survivors: 15.0M->35.0M
# Survivor 区:15MB → 35MB
Heap: 2548.0M(4096.0M)->1323.0M(4096.0M)]
# 堆:使用 2.5GB → 1.3GB
[Times: user=0.80 sys=0.10, real=0.15 secs]
# 停顿 150ms
# Mixed GC 日志
2024-01-15T10:35:00.000: [GC pause (G1 Evacuation Pause) (mixed)
# 混合回收
(Eden: 1100.0M(1180.0M)->0.0B(1200.0M)
Survivors: 35.0M->40.0M
Heap: 3200.0M(4096.0M)->1800.0M(4096.0M)
# 包含老年代 Region 的回收
[Times: user=1.20 sys=0.20, real=0.30 secs]G1 vs CMS
| 对比项 | G1 | CMS |
|---|---|---|
| 设计理念 | 可预测停顿 | 低停顿 |
| 分代 | Region 化分代 | 传统分代 |
| 内存碎片 | 无(复制整理) | 有(标记-清除) |
| Full GC | 有(但少) | 有(Serial Old) |
| 并发阶段 | 全程并发 | 部分并发 |
| 停顿时间 | 可配置 | 不可控 |
| 适用版本 | JDK 9+ | JDK 8 及之前 |
| 默认收集器 | JDK 9+ 是 | 否 |
面试追问方向
- G1 的 Region 大小是如何计算的?为什么不能手动设置得太小或太大?
- G1 的 RSet 是什么?为什么需要它?有什么开销?
- G1 的停顿时间目标是如何实现的?「回收价值最高」怎么衡量?
- Humongous Region 是什么?为什么它可能导致 Full GC?
- G1 的 Mixed GC 什么时候触发?和 CMS 的老年代收集有什么区别?
- JDK 11+ 的 ZGC 和 G1 相比,各自的适用场景是什么?
