G1垃圾收集器原理与调优

G1垃圾收集器原理与调优

G1(Garbage First)是 JDK 9+ 的默认垃圾收集器,设计目标是替代 CMS,在可预测的停顿时间内获得最高吞吐量。

G1 的设计目标

  1. 可预测的停顿时间:设置目标停顿时间,G1 尽量遵守
  2. 高吞吐量:整体吞吐量不低于 CMS
  3. 无内存碎片:采用复制算法,整理内存

Region 化的堆结构

┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ E │ S │ O │ O │ H │ E │ S │ O │ E │ O │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

E = Eden Region(年轻代)
S = Survivor Region
O = Old Region(老年代)
H = Humongous Region(大对象,占连续多个Region)

Region大小:1MB, 2MB, 4MB, 8MB, 16MB, 32MB

大对象处理

  • 对象 > 0.5 Region:放入 Humongous Region
  • 对象 > 1 Region:占用连续多个 Humongous Region
  • Humongous Region 直接分配在老年代

核心数据结构

Remembered Set(记忆集)

每个Region维护一个RSet,记录哪些Region引用了本Region的对象

Region A (Old)
├── RSet: [Region B, Region C]
│ └── Region B (Eden) 引用了 A 中的对象
│ └── Region C (Old) 引用了 A 中的对象

作用:避免全堆扫描,Minor GC 时只需扫描相关 Region。

Collection Set(CSet)

每次 GC 选择的一组 Region,是需要被回收的候选区域。

G1 的回收过程

Young GC

阶段1:选择所有 Eden + Survivor Region 进入 CSet
阶段2:复制存活对象到新的 Survivor / Old Region
阶段3:清空回收的 Region

Eden Region × 10 + Survivor Region × 2

复制存活对象

Survivor Region × 1(或晋升到 Old)

特点

  • 完全 stop-the-world
  • 多线程并行复制
  • 目标是尽快完成

Concurrent Marking Cycle(并发标记周期)

当堆内存使用率达到阈值时触发:

1. Initial Mark(初始标记)

STW,标记 GC Roots 直接引用的对象
依附于 Young GC,几乎无额外开销

2. Root Region Scanning(根区域扫描)

扫描 Survivor Region 中引用老年代的对象
并发执行,必须在 Young GC 前完成

3. Concurrent Mark(并发标记)

从 GC Roots 开始遍历整个堆,标记存活对象
与用户线程并发执行
使用 SATB(Snapshot-At-The-Beginning)算法

SATB 算法

  • 标记开始时做一个快照
  • 并发期间新分配的对象默认存活
  • 通过 write barrier 记录引用变化

4. Remark(重新标记)

STW,处理 SATB 队列中的引用变化
比 CMS 的重新标记快(SATB 比 Incremental Update 高效)

5. Cleanup(清理)

STW,统计存活对象,识别完全空闲的 Region
并发清理空闲 Region

Mixed GC

并发标记完成后,G1 知道哪些 Region 垃圾最多,开始 Mixed GC:

回收策略:
1. 所有 Eden Region(必须回收)
2. 所有 Survivor Region(必须回收)
3. 部分垃圾最多的 Old Region(根据停顿目标选择数量)

目标:在 MaxGCPauseMillis 内回收尽可能多的垃圾

关键参数

基本参数

# 启用G1
-XX:+UseG1GC

# 目标最大停顿时间(默认200ms)
-XX:MaxGCPauseMillis=200

# Region大小(默认根据堆大小计算)
-XX:G1HeapRegionSize=16m

# 触发并发标记的堆占用率(默认45%)
-XX:InitiatingHeapOccupancyPercent=45

进阶参数

# 年轻代大小(G1会自动调整,不建议手动设置)
# -Xmn 或 -XX:NewRatio 不推荐与G1一起使用

# 保留多少空闲Region(默认10%)
-XX:G1ReservePercent=10

# 并发线程数
-XX:ConcGCThreads=4

# 并行线程数
-XX:ParallelGCThreads=8

# 每个Region的 Remembered Set 占用空间(默认5%)
-XX:G1RSetUpdatingPauseTimePercent=5

# 大对象阈值(Region大小的百分比,默认50%)
-XX:G1HeapWastePercent=5

# 触发Mixed GC的阈值(默认老年代占比45%)
-XX:G1MixedGCCountTarget=8
-XX:G1MixedGCLiveThresholdPercent=85

调优策略

1. 调整停顿时间目标

# 如果 GC 频繁但停顿短
-XX:MaxGCPauseMillis=100

# 如果 GC 次数少但停顿长
-XX:MaxGCPauseMillis=500

注意:停顿时间和吞吐量是反比关系,需要平衡。

2. 避免大对象

// 大对象直接进入Humongous Region,难以回收
byte[] hugeArray = new byte[100 * 1024 * 1024]; // 100MB

// 解决方案:拆分为小对象或复用缓冲区

3. 监控指标

# 打印GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log

# JDK 9+ 统一日志
-Xlog:gc*:file=/path/to/gc.log:time,uptime,level,tags

4. 常见问题

Evacuation Failure(晋升失败)

日志:to-space exhausted
原因:复制存活对象时,Survivor/Old 空间不足
解决:
- 增大堆内存
- 降低 MaxGCPauseMillis(回收更多Region)
- 提前触发并发标记(降低 IHOP)

Humongous Allocation Failure

日志:G1 Humongous Allocation
原因:大对象分配失败
解决:
- 避免大对象
- 增大Region大小
- 增大堆内存

G1 vs CMS

特性 G1 CMS
算法 复制+整理 标记-清除
碎片
停顿时间 可预测 较短但不可预测
内存占用 较高(RSet) 较低
大对象 Region化 直接入Old
调优复杂度 简单 复杂
推荐版本 JDK 9+ JDK 8 及之前

生产环境配置示例

# 16GB 堆内存,目标停顿 200ms
java \
-Xms16g -Xmx16g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=35 \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:/var/log/app/gc.log \
-jar application.jar

总结

G1 通过 Region 化和 Remembered Set,实现了:

  1. 可预测的停顿时间:控制每次回收的 Region 数量
  2. 无内存碎片:复制算法整理内存
  3. 高并发友好:多数阶段与用户线程并发

对于大多数现代 Java 应用,G1 是首选收集器。调优的核心是找到停顿时间和吞吐量的最佳平衡点。


   转载规则


《G1垃圾收集器原理与调优》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录