跳到主要内容
EN
返回

G1GC 原理与调优:从 Region 到 Humongous

Zhou Xunyou 9 分钟阅读 jvm
分享

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 定位生产环境中的内存问题。

评论