G1 垃圾收集器
JDK 9 把 G1 变成了默认垃圾收集器。
这意味着:从 JDK 9 开始,如果你在启动参数里没有指定 -XX:+UseG1GC,你用的就是 G1。
为什么是 G1
之前的默认收集器是 CMS,但 CMS 有两个致命问题:
- 内存碎片化:长时间运行后,内存碎片化严重
- 并发失败:如果并发标记跟不上垃圾产生的速度,就会退化成串行 Full GC,停顿时间可能达到几秒
G1 的设计目标:可预测的停顿时间 + 高吞吐量。
G1 的核心思路
分区(Region)
G1 把堆内存分成多个大小相等的 Region(默认 1MB-32MB):
┌─────────────────────────────────────────────────────────────────┐
│ G1 Heap │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Eden │ │ Eden │ │ Survivor│ │ Eden │ │
│ │ Region │ │ Region │ │ Region │ │ Region │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Survivor│ │ Survivor│ │ Old │ │ Old │ │
│ │ Region │ │ Region │ │ Region │ │ Region │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌─────────────────────────┐ ┌─────────────┐ │
│ │ Humongous Region │ │ Free │ │
│ │ (> 50% Region) │ │ Region │ │
│ └─────────────────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘Region 的大小是固定的(由 -XX:G1HeapRegionSize 设置),这解决了 CMS 的内存碎片问题。
停顿时间预测
G1 会跟踪每个 Region 的回收价值(回收多少垃圾 + 花多少时间),然后优先回收价值最高的 Region:
┌─────────────────────────────────────────────────────────────────┐
│ 停顿时间预测模型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ G1 维护一个 Region 列表,按回收价值排序: │
│ │
│ Region E (价值: 80MB/5ms) ←── 优先回收 │
│ Region A (价值: 60MB/4ms) │
│ Region B (价值: 40MB/3ms) │
│ Region C (价值: 20MB/4ms) │
│ │
│ 目标停顿时间: 200ms │
│ 回收 E + A + B = 180MB / 12ms ←── 在目标内 │
│ │
└─────────────────────────────────────────────────────────────────┘你可以通过 -XX:MaxGCPauseMillis=200 设置目标停顿时间(默认 200ms)。
年轻代收集
G1 的年轻代也是 Eden + Survivor 的结构,只是用 Region 来表示:
bash
# 年轻代初始大小
java -Xms4g -Xmx4g \
-XX:NewSize=1g -XX:MaxNewSize=1g \
-XX:+UseG1GC \
-jar app.jar
# 设置目标停顿时间
java -XX:MaxGCPauseMillis=200 \
-XX:+UseG1GC \
-jar app.jar年轻代收集(Young GC):
Young GC 前:
┌──────┬──────┬──────┬──────┬──────┐
│ Eden │ Eden │ S0 │ S1 │ Old │
│ 60% │ 40% │ 80% │ 0% │ 45% │
└──────┴──────┴──────┴──────┴──────┘
Young GC 后:
┌──────┬──────┬──────┬──────┬──────┐
│ Eden │ Eden │ S0 │ S1 │ Old │
│ 0% │ 10% │ 20% │ 0% │ 48% │
└──────┴──────┴──────┴──────┴──────┘混合收集(Mixed GC)
当老年代占比超过阈值(InitiatingHeapOccupancyPercent,默认 45%),G1 会触发并发标记周期:
并发标记周期:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 1. 初始标记 (STW) - 标记 GC Roots 直接引用的对象 │
│ ↓ │
│ 2. 并发标记 - 遍历对象图,记录存活对象 │
│ ↓ │
│ 3. 最终标记 (STW) - 完成标记,处理漏掉的引用 │
│ ↓ │
│ 4. 筛选回收 (STW) - 按回收价值排序,回收 selected Set │
│ │
└─────────────────────────────────────────────────────────────────┘标记完成后,G1 会执行混合收集,回收年轻代 + 部分老年代 Region。
调优参数
核心参数
bash
# 启用 G1(JDK 9+ 默认启用)
java -XX:+UseG1GC -jar app.jar
# 目标停顿时间
java -XX:MaxGCPauseMillis=200 -XX:+UseG1GC -jar app.jar
# Heap Region 大小(1MB, 2MB, 4MB, 8MB, 16MB, 32MB)
java -XX:G1HeapRegionSize=16m -XX:+UseG1GC -jar app.jar
# 触发混合回收的老年代阈值
java -XX:InitiatingHeapOccupancyPercent=50 -XX:+UseG1GC -jar app.jar
# 混合回收时最多回收多少个老年代 Region
java -XX:G1MixedGCCountTarget=8 -XX:+UseG1GC -jar app.jar完整配置示例
bash
java -Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=4m \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:G1MixedGCCountTarget=8 \
-XX:G1HeapWastePercent=5 \
-XX:+ParallelRefProcEnabled \
-Xlog:gc*:file=gc.log \
-jar app.jar参数解释
| 参数 | 默认值 | 说明 |
|---|---|---|
MaxGCPauseMillis | 200ms | 目标停顿时间 |
G1HeapRegionSize | 自动 | Region 大小 |
InitiatingHeapOccupancyPercent | 45% | 触发并发标记的阈值 |
G1MixedGCCountTarget | 8 | 混合回收次数 |
G1HeapWastePercent | 5% | 允许的浪费空间比例 |
监控与诊断
GC 日志
bash
# 详细 GC 日志
java -Xlog:gc*=debug:file=gc.log \
-XX:+UseG1GC \
-jar app.jar
# 实时查看
jstat -gcutil <pid> 1000
# 使用 G1CardTables
jinfo -flag G1HeapRegionSize <pid>GC 日志分析
[GC pause (G1 Evacuation Pause) (young)]
GC concurrent humongous allocation.关键指标:
- Evacuation Pause:年轻代或混合收集的停顿
- Humongous Allocation:分配大对象(超过 50% Region)
- Conc Marking:并发标记周期
常用诊断命令
bash
# 查看当前 GC 配置
jinfo -flags <pid>
# 查看堆使用
jhsdb jmap --heap --pid <pid>
# 分析 GC 原因
jcmd <pid> GC.class_histogram
jcmd <pid> GC.heap_info常见问题与解决
问题一:Full GC 频繁
原因:对象分配速率过高,混合回收来不及回收老年代
解决:
1. 增大堆内存 -Xms4g -Xmx4g
2. 调整目标停顿时间 -XX:MaxGCPauseMillis=300
3. 降低触发阈值 -XX:InitiatingHeapOccupancyPercent=50问题二:停顿时间过长
原因:Region 太大或太小,或混合回收选择的 Region 太多
解决:
1. 减小 Region 大小 -XX:G1HeapRegionSize=2m
2. 减小目标停顿时间让 G1 更保守
3. 减少混合回收次数 -XX:G1MixedGCCountTarget=4问题三:大对象分配失败
原因:Humongous Region 碎片化
解决:
1. 增大 Region 大小 -XX:G1HeapRegionSize=16m
2. 减少大对象创建小结
G1 是 JDK 9+ 的默认收集器,适合大多数场景:
| 场景 | 推荐配置 |
|---|---|
| 通用场景 | 默认配置即可 |
| 低延迟需求 | -XX:MaxGCPauseMillis=100 |
| 大内存 | -Xms8g -Xmx8g -XX:G1HeapRegionSize=8m |
| 高吞吐量 | 考虑 Parallel GC |
记住:不要过早优化。先跑默认配置,用 GC 日志观察表现,确实有问题再调参。
