G1GC 原理与调优:从 Region 到 Humongous
G1(Garbage-First)GC 是 JDK 9+ 的默认垃圾收集器,也是目前最流行的企业级 GC。本文深入解析 G1 的工作原理和调优方法。
为什么需要 G1?
传统的 GC(如 Parallel GC、CMS)都面临两个问题:
- 停顿时间不可控:Parallel GC 追求高吞吐但停顿时间长(Full GC 可达数秒)
- 内存碎片化:CMS 产生碎片,长期运行后触发 Full GC 整理
G1 的设计目标:可预测的停顿时间(Pause Time Target)+ 无内存碎片。
G1 的核心思想:Region
G1 将整个堆划分为多个等大小的 Region(每个 Region 大小 = 堆大小 / 2048,默认 1MB~32MB):
G1 堆内存划分为 N 个 Region(以 4GB 堆为例,2048 个 Region,每个约 2MB):
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ Eden │ │ Eden │ │ Eden │ │ S0 │ │ S1 │ │ Old │
│ (R1) │ │ (R2) │ │ (R3) │ │ (R4) │ │ (R5) │ │ (R6) │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ Old │ │ Old │ │ Humo │ │ Humo │ │ Free │ │ Free │
│ (R7) │ │ (R8) │ │ (R9) │ │(R10) │ │(R11) │ │(R12) │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘
Eden Region:新建对象分配区
Survivor Region:存放 Minor GC 后存活的对象
Old Region:长期存活的对象
Humongous Region:存放大对象(超过 Region 50% 的对象)
Free Region:空闲区域
Humongous 对象
当对象大小 超过 Region 大小的 50% 时,G1 会分配专门的 Humongous Region。这些对象直接分配在老年代,会跳过 Minor GC,只有 Full GC 时才会清理。
注意:Humongous 对象过多会影响 G1 的效率,因为 G1 无法对Humongous Region 做 compaction。如果有大数组场景,建议将
-XX:G1HeapRegionSize调大。
G1 的垃圾收集周期
G1 的收集周期由多个 Mixed GC 组成,不是单纯的 Minor GC 或 Major GC。
G1 收集周期:
┌─ Full GC ─────────────────┐
│ (发生于回收不及 时) │
│ │
┌─ Initial Mark ────┤ │
│ (标记 Old Gen 中 │ │
│ 有引用的 Survivor │ │
│ roots) │ │
│ Stop-the-World │ │
└──────────────────────┘ │
▼
┌─────────────────────────────────────────────────────────────┐
│ Young GC (Minor GC) — 纯 Copy,到达年龄晋升到 Old │
│ └─ Concurrent Marking (并发标记) — 与应用线程并行 │
│ └─ Mixed GC — Young + 部分 Old Region 一起回收 │
│ └─ 回到 Young GC 循环 │
└─────────────────────────────────────────────────────────────┘
三色标记(Tri-color Marking)
G1 的并发标记阶段使用三色标记算法:
白色(White) :尚未被扫描的对象,GC 后视为垃圾
灰色(Gray) :已被发现但其引用的对象尚未全部扫描
黑色(Black) :已被完全扫描,所有引用都已记录
GC Roots(黑) ──引用──→ 对象A(灰) ──引用──→ 对象B(白)
↓
已发现但引用 尚未发现
未完全扫描
并发标记的问题:在标记过程中,应用线程可能修改引用关系,导致”漏标”或”错标”。G1 通过 SATB(Snapshot-At-The-Beginning) 算法解决这个问题——在并发阶段,将引用变更前的状态记录下来,确保不会漏标。
关键调优参数
停顿时间目标(最重要)
# 设置目标停顿时间(G1 会尽量满足,默认为 200ms)
-XX:MaxGCPauseMillis=200
# 设置期望的停顿时间抖动范围
-XX:GCPauseIntervalMillis=1000
G1 根据历史数据动态调整回收范围——如果上次停顿时间是 180ms,下次可能只回收一部分 Old Region 来控制时间。
堆内存与 Region 大小
# 初始堆和最大堆建议保持一致,避免运行时调整
-Xms4g -Xmx4g
# Region 大小,默认为堆大小/2048,最小 1MB,最大 32MB
# 如果对象较大,适当调大可以减少 Humongous Region 数量
-XX:G1HeapRegionSize=4m
Mixed GC 调优
# Mixed GC 触发时,Old Region 的回收比例
# 默认 5%,即每次 Mixed GC 回收 5% 的 Old Region
-XX:G1OldCSetRegionThresholdPercent=10
# 在 Young GC 后,跟随一次 Mixed GC 之前,Young 区最大占比
# 防止 Young 区过大导致 Mixed GC 停顿时间超标
-XX:G1MaxNewSizePercent=60
# Mixed GC 最少包含多少个 Old Region 才触发
-XX:G1MixedGCCountTarget=8
并发标记调优
# 并发标记线程数,默认 = (ParallelGCThreads + 2) / 4
-XX:ConcGCThreads=4
# 标记开始前强制执行一次 Young GC(减少 Remark 的工作量)
-XX:+AlwaysPreTouch
常见问题与解决方案
1. G1 退化为 Full GC
现象:GC pause (Full GC) 日志频繁出现,停顿时间很长。
原因:
- 对象分配速率过高,Mixed GC 来不及回收
- Humongous 对象过多,堆碎片化
- 显式调用
System.gc()
解决:
# 增大堆或提高 G1 收集速度
-Xms8g -Xmx8g
-XX:MaxGCPauseMillis=300
-XX:+UseG1GC
# 减少 Humongous 对象的产生(避免大数组直接分配)
# 如果无法避免,调大 Region 大小
-XX:G1HeapRegionSize=8m
# 添加参数减少显式 GC 的影响
-XX:+DisableExplicitGC
2. 停顿时间过长
原因:目标停顿时间设置过短,而老年代数据量大。
解决:
# 放宽停顿时间目标,允许更长停顿换取更好吞吐
-XX:MaxGCPauseMillis=500
# 提高 Mixed GC 频率,减少每次回收的 Region 数量
-XX:G1MixedGCCountTarget=16
-XX:G1OldCSetRegionThresholdPercent=3
# 减少 HeapRegion 大小,使每次回收粒度更细
-XX:G1HeapRegionSize=2m
3. GC 日志分析
# 开启详细 GC 日志
-Xlog:gc*=debug:file=gcdetail.log:time,level,tags:filecount=5,filesize=10m
# 简化 GC 日志
-Xlog:gc*:file=gc.log:time
# 使用 G1EvacFailure 追踪晋升失败
-Xlog:gc+marking=trace:file=marking.log
# 从日志中提取停顿时间分布
grep "pause" gcdetail.log | awk '{print $NF}' | sort -n
调优检查清单
1. 确认停顿目标:-XX:MaxGCPauseMillis=XXX
2. 堆大小一致:-Xms=-Xmx
3. Region 大小合理:-XX:G1HeapRegionSize
4. Mixed GC 频率:-XX:G1MixedGCCountTarget
5. 老年代回收比例:-XX:G1OldCSetRegionThresholdPercent
6. 监控 Humongous 对象比例(> 3% 需关注)
7. 避免显式 System.gc()
总结
- G1 将堆划分为 Region,实现了可预测停顿时间和无内存碎片
- Humongous 对象是 G1 的特殊区域,跳过 Minor GC
- 调优核心是 MaxGCPauseMillis,G1 会自动调整回收范围
- 避免 Full GC 的关键是对象分配速率 < 回收速率
下篇将介绍内存泄漏排查工具与实战——用 MAT、Async-profiler 定位生产环境中的内存问题。
评论