6.G1垃圾回收器

#jvm #gc #g1

总结
  • G1 把堆拆成等大的 Region,不再是连续的年轻代/老年代,靠回收价值优先策略控制停顿时间
  • Young GC 回收年轻代,Mixed GC 同时回收年轻代 + 部分老年代,Full GC 是兜底
  • 核心调优参数只有三个:-XX:MaxGCPauseMillis-XX:G1HeapRegionSize-XX:InitiatingHeapOccupancyPercent
  • 停顿时间目标设太激进会适得其反,200ms 是个合理起点
  • 堆超过 16G 且停顿要求更严格,考虑升级到 5.ZGC学习

1. G1 和传统 GC 的区别

传统的 ParNew + CMS 把堆物理上分成年轻代和老年代两块连续区域,年轻代满了触发 Young GC,老年代满了触发 Full GC,停顿时间和堆大小强相关,堆越大停顿越长。

G1(Garbage First)把整个堆切成 2048 个等大的 Region,每个 Region 逻辑上扮演 Eden、Survivor 或 Old 的角色,但物理上可以不连续。GC 时优先回收"垃圾最多、回收价值最高"的 Region,在可控的停顿时间内尽量多回收内存。

传统 GC 的详细对比见 3.垃圾回收#4.1 ParNew + CMS(JDK 8 常用)

2. Region 模型

堆内存(比如 8G)
┌──────────────────────────────────────────────┐
│ E  E  E  S  S  O  O  O  E  H  O  E  S  O  … │
└──────────────────────────────────────────────┘
  E=Eden  S=Survivor  O=Old  H=Humongous(大对象)

3. GC 类型

3.1 Young GC

只回收 Eden 和 Survivor Region,存活对象复制到新的 Survivor 或晋升 Old。触发条件:Eden Region 用完。

这个阶段是 STW 的,停顿时间通常几十毫秒,G1 会根据 MaxGCPauseMillis 动态调整年轻代 Region 数量来控制停顿。

3.2 Mixed GC

同时回收年轻代 + 部分老年代 Region。触发条件:老年代占堆的比例超过 InitiatingHeapOccupancyPercent(默认 45%)。

Mixed GC 分两个阶段:

flowchart TD
    A[老年代占比超过 IHOP 阈值] --> B[并发标记周期]
    B --> B1[初始标记 STW 很短]
    B1 --> B2[并发标记 与业务线程同时跑]
    B2 --> B3[最终标记 STW 很短]
    B3 --> B4[清理 统计各 Region 回收价值]
    B4 --> C[Mixed GC 阶段]
    C --> C1[Young GC + 回收价值高的 Old Region]
    C1 --> C2{老年代占比降到安全水位?}
    C2 -- 否 --> C1
    C2 -- 是 --> D[结束 Mixed GC]

3.3 Full GC

G1 的兜底机制,单线程 Serial Old,停顿时间很长。触发场景:

Full GC 是需要极力避免的,出现 Full GC 说明 G1 的并发回收跟不上分配速度,需要调参或扩内存。

4. 核心参数详解

4.1 停顿时间目标

-XX:MaxGCPauseMillis=200   # 默认 200ms

G1 会根据这个目标动态调整年轻代大小和每次 Mixed GC 回收的 Old Region 数量。

注意:这是个"尽力而为"的目标,不是硬保证。设太小(比如 50ms)反而会让 G1 频繁 GC、吞吐量下降。一般建议:

4.2 Region 大小

-XX:G1HeapRegionSize=4m   # 建议显式设置,不依赖自动计算

Region 太小:Region 数量多,管理开销大,大对象容易变成 Humongous。
Region 太大:回收粒度粗,停顿时间难以控制。

经验值:堆 4G 以下用 2M,4~8G 用 4M,8G 以上用 8M 或 16M。

4.3 Mixed GC 触发阈值

-XX:InitiatingHeapOccupancyPercent=45   # 默认 45%

老年代占整个堆的比例超过这个值,触发并发标记周期,进而触发 Mixed GC。

调低(比如 30%):更早触发 Mixed GC,老年代压力小,但 GC 更频繁。
调高(比如 60%):减少 GC 频率,但老年代压力大,容易触发 Full GC。

4.4 其他常用参数

-XX:G1MixedGCCountTarget=8          # 一次并发标记后,分几次 Mixed GC 完成回收,默认 8
-XX:G1HeapWastePercent=5            # 老年代可回收垃圾低于 5% 时停止 Mixed GC,默认 5%
-XX:G1OldCSetRegionThresholdPercent=10  # 每次 Mixed GC 最多回收老年代 Region 的比例,默认 10%
-XX:ConcGCThreads=4                 # 并发标记线程数,默认 CPU核数/4

5. 生产环境推荐配置

5.1 通用模板(JDK 11+)

-XX:+UseG1GC
-Xms8g -Xmx8g                              # 堆大小固定,避免动态扩缩
-XX:G1HeapRegionSize=4m
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=40       # 比默认值低一点,更早触发 Mixed GC
-XX:G1MixedGCCountTarget=8
-XX:G1HeapWastePercent=5
-XX:ConcGCThreads=4
-XX:+ParallelRefProcEnabled                # 并行处理引用,减少 STW 时间
-XX:+UnlockExperimentalVMOptions
-XX:G1NewSizePercent=20                    # 年轻代最小占比 20%
-XX:G1MaxNewSizePercent=40                 # 年轻代最大占比 40%

5.2 GC 日志(必须开启,用于排查问题)

# JDK 9+
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=50m

# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
-Xloggc:/logs/gc.log
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50m

5.3 大堆场景(16G+)

-XX:+UseG1GC
-Xms16g -Xmx16g
-XX:G1HeapRegionSize=8m                    # Region 调大
-XX:MaxGCPauseMillis=300
-XX:InitiatingHeapOccupancyPercent=35
-XX:ConcGCThreads=8

堆超过 32G 且停顿要求在 10ms 以内,建议直接上 [[5.ZGC学习]]。

6. 常见问题排查

6.1 频繁 Full GC

先看 GC 日志确认是哪种 Full GC:

[GC pause (G1 Evacuation Pause)]   → 正常 Young/Mixed GC
[Full GC (Allocation Failure)]     → 分配失败,内存不够
[Full GC (Ergonomics)]             → G1 自己判断需要 Full GC

排查方向:

6.2 Young GC 停顿过长

年轻代 Region 太多,每次 Young GC 要扫描的 Region 多。

-XX:G1NewSizePercent=10    # 调小年轻代最小占比
-XX:G1MaxNewSizePercent=30 # 调小年轻代最大占比

6.3 Mixed GC 不触发

老年代一直涨但没有触发 Mixed GC,通常是 InitiatingHeapOccupancyPercent 设太高了,或者并发标记线程数不够。

-XX:InitiatingHeapOccupancyPercent=30   # 调低触发阈值
-XX:ConcGCThreads=6                     # 增加并发标记线程

7. G1 vs CMS vs ZGC

对比项 CMS G1 ZGC
适用 JDK 8 9+ 15+(生产可用)
堆大小 < 4G 4G~32G 不限
停顿时间 几十~几百 ms 可控,通常 < 200ms < 10ms
吞吐量 略低(读屏障开销)
内存碎片
调优难度
Full GC 有(概率低) 极少

调优参数细节见 4.JVM调优