JVM G1 切 ZGC 两周调参实录:p99 GC 暂停 80ms→3ms

JVM Full GC 每分钟一次,p99 飙 800ms。G1 调参 + 切 ZGC 全实录:JVM GC 算法对比 + G1 关键参数 + ZGC 染色指针 + 内存增加权衡 + NMT 排查 + JFR + 监控告警 + 不同场景 GC 选型。p99 暂停 80ms→3ms。

2023 年我们一个 Java 微服务突然 GC 频繁,Full GC 每分钟一次,业务 p99 延迟从 50ms 飙到 800ms。从 G1 切到 ZGC 后 GC 暂停时间从 200ms 降到 3ms,但花了两周调参才稳定。本文复盘 G1 调优 + ZGC 切换的全过程,讲透 JVM 各种 GC 算法对比、调参方法论、实战监控。

问题现象

服务:订单查询(Spring Boot 2.7 + JDK 11)
配置:Pod 8C 16G,Xmx 12g,G1 GC

突然出现:
- GC 频率:1 min/次 Full GC
- GC 暂停:200-500ms
- 业务 p99:从 50ms → 800ms
- CPU:50% → 90%(GC 线程占用)

排查日志:
2023-10-15T03:15:23.421+0800 Full GC (Allocation Failure)
  [PSYoungGen: 4096M->0M(5120M)]
  [ParOldGen: 7168M->6900M(7168M)] 12288M->6900M(12288M),
  [Metaspace: 245M->245M(1024M)], 0.4823 secs

老年代占用 7168M(已满),触发 Full GC

JVM GC 算法对比

GC 算法                    适用场景              暂停时间       吞吐量
==========================================================
Serial GC                   单核小堆              长(秒级)      高
ParallelGC (PS+PO)          吞吐量优先(老 default) 中(100ms+)   最高
CMS(已废弃)                响应时间(JDK 8 流行)  短(50-200ms) 中
G1 GC(JDK 9+ default)       通用,大堆            中(<200ms)    高
ZGC(JDK 11+ 试验,15 GA)    超大堆,亚毫秒暂停    极短(<10ms)   中
Shenandoah(OpenJDK)         类似 ZGC,响应时间    极短(<10ms)   中
Epsilon GC                  压测专用,不回收      无             最高(短时)

JDK 21 默认 G1,ZGC 已 GA 推荐生产用

G1 调优(第一阶段)

# 默认配置
JAVA_OPTS="-Xms12g -Xmx12g -XX:+UseG1GC"

# 排查 GC 日志开启
JAVA_OPTS="$JAVA_OPTS \
  -Xlog:gc*:file=/var/log/jvm/gc.log:time,uptime,level,tags:filecount=10,filesize=100M \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/jvm/heapdump.hprof \
  -XX:+ExitOnOutOfMemoryError"

# G1 调参 - 控制暂停时间
JAVA_OPTS="$JAVA_OPTS \
  -XX:MaxGCPauseMillis=100 \              # 目标最大暂停 100ms
  -XX:G1HeapRegionSize=16m \              # region 大小
  -XX:G1NewSizePercent=20 \               # 年轻代最小占比
  -XX:G1MaxNewSizePercent=40 \            # 年轻代最大占比
  -XX:InitiatingHeapOccupancyPercent=45 \ # 老年代 45% 启动 mixed GC
  -XX:G1MixedGCCountTarget=8 \            # mixed GC 分 8 次完成
  -XX:G1MixedGCLiveThresholdPercent=85 \  # 老年代 region 存活 < 85% 才回收
  -XX:G1HeapWastePercent=5 \              # 浪费 5% 不再 mixed GC
  -XX:G1OldCSetRegionThresholdPercent=10  # 单次 mixed 回收 10% region"

# 大对象阈值(>region/2)
JAVA_OPTS="$JAVA_OPTS \
  -XX:G1HeapRegionSize=32m"   # region 32M,大对象 > 16M 进 Humongous

# 并发线程
JAVA_OPTS="$JAVA_OPTS \
  -XX:ParallelGCThreads=8 \             # STW 阶段并行线程
  -XX:ConcGCThreads=2 \                 # 并发标记线程"

看 GC 日志

# gc.log 关键指标
$ tail -f /var/log/jvm/gc.log

[2023-10-15T03:15:23.421+0800][info][gc] GC(1234) Pause Young (Normal) (G1 Evacuation Pause) 8192M->6500M(12288M) 45.123ms

[2023-10-15T03:16:01.234+0800][info][gc] GC(1235) Pause Remark 6500M->6500M(12288M) 12.456ms

[2023-10-15T03:16:30.567+0800][info][gc] GC(1236) Pause Cleanup 6500M->6500M(12288M) 5.789ms

[2023-10-15T03:17:00.123+0800][info][gc] GC(1237) Pause Mixed (G1 Evacuation Pause) 7800M->4500M(12288M) 89.234ms

# 用 gceasy.io 可视化
$ curl -F "file=@/var/log/jvm/gc.log" https://api.gceasy.io/analyzeGC -H "API-KEY: ${KEY}"

# 关键指标:
# - GC 频率(次/分钟)
# - 平均暂停(ms)
# - p99 暂停(ms)
# - 吞吐量(应用时间 / 总时间)
# - 老年代使用率趋势
# - Allocation Rate / Promotion Rate

G1 调优结果

指标                调优前          调优后(G1)
=============================================
Young GC 频率       5s/次           10s/次
Young GC 暂停       80ms            50ms
Mixed GC 频率       30s/次          120s/次
Mixed GC 暂停       300ms           80ms
Full GC 频率        1 min/次        0(无 Full GC)
业务 p99            800ms           120ms

仍不够好:
- 80ms 的 Mixed GC 影响 p99
- 大堆(12G)时 G1 暂停时间还是大
- 目标 p99 < 50ms 还差

决定试 ZGC

切换 ZGC

# JDK 17+ ZGC GA
$ java -version
openjdk version "17.0.9"

# 启用 ZGC
JAVA_OPTS="$JAVA_OPTS \
  -XX:+UseZGC \                          # 用 ZGC
  -XX:+ZGenerational \                   # JDK 21+ 分代 ZGC(强烈推荐)
  -Xms12g -Xmx12g"

# ZGC 几乎不需要调参!只有这些可以调:
JAVA_OPTS="$JAVA_OPTS \
  -XX:ConcGCThreads=4 \                  # 并发 GC 线程,默认 1/8 CPU
  -XX:ParallelGCThreads=8 \              # STW 并行
  -XX:SoftMaxHeapSize=11g \              # 软上限(留 1G 给 buffer)
  -XX:ZAllocationSpikeTolerance=2.0 \    # 分配尖峰容忍
  -XX:+UseLargePages \                   # 大页(2MB Linux hugepage)
  -XX:+UseTransparentHugePages"

# Linux 大页配置
$ sysctl -w vm.nr_hugepages=8192
# 16G 堆需要 8192 个 2MB 大页 = 16GB

# 验证 GC 类型
$ jcmd  VM.flags | grep -i gc
   -XX:+UseZGC
   -XX:+ZGenerational

ZGC 日志

# ZGC 日志格式
[2023-10-22T10:23:15.123+0800][info][gc] GC(12) Garbage Collection (Allocation Rate) 7800M(63%)->4500M(36%)

[2023-10-22T10:23:15.234+0800][info][gc,phases] GC(12) Pause Mark Start 0.456ms

[2023-10-22T10:23:15.567+0800][info][gc,phases] GC(12) Concurrent Mark 234.123ms

[2023-10-22T10:23:15.789+0800][info][gc,phases] GC(12) Pause Mark End 0.234ms

[2023-10-22T10:23:15.987+0800][info][gc,phases] GC(12) Pause Relocate Start 1.123ms

# 关键:Pause 阶段都 < 2ms,Concurrent 阶段不阻塞业务
# GC 总耗时:几百毫秒,但暂停只 < 10ms

# ZGC stats
$ jcmd  GC.heap_info
 garbage-first heap   total 12582912K, used 4521376K [...]
 ZGC: phase 0, ...

# 持续监控 GC pause
$ jstat -gc  1000
   S0     S1     E       O       M     P     YGC   YGCT  FGC  FGCT
   0.0    0.0    0.0     0.0     ...   ...   123   0.45   3    0.012

ZGC 切换的坑

坑 1:内存占用增加

G1 时 Xmx 12g,常驻内存 13G
切 ZGC 后常驻内存 18G

原因:ZGC 用染色指针,需要额外 metadata
ZGC 的 reservation factor 默认 2x(为整理预留空间)

修法:
1. SoftMaxHeapSize 限制软上限
2. Pod limits 调整(15G → 20G)
3. 大页可以减少 page table 开销

坑 2:Native memory leak

# 切到 ZGC 后,容器 RSS 持续涨
# 但 -Xmx 12g 没变,GC 也正常
# 看 NMT(Native Memory Tracking)

$ jcmd  VM.native_memory summary

Total: reserved=21342MB, committed=20123MB
-                 Java Heap (reserved=12288MB, committed=12288MB)
-                     Class (reserved=1283MB, committed=312MB)
-                    Thread (reserved=523MB, committed=523MB)
-                       NMT (reserved=128MB, committed=128MB)
-                      Code (reserved=251MB, committed=128MB)
-                        GC (reserved=4523MB, committed=4523MB)   ← 大
-                  Compiler (reserved=23MB, committed=23MB)
-                  Internal (reserved=523MB, committed=523MB)
-                    Symbol (reserved=85MB, committed=85MB)
-    Native Memory Tracking (reserved=23MB, committed=23MB)
-               Arena Chunk (reserved=12MB, committed=12MB)

# GC 区域 4.5GB 是正常的(ZGC 元数据)
# 不是 leak,是 ZGC 的代价

坑 3:JFR 不兼容

原 JFR(Java Flight Recorder)在 G1 上的 GC 事件,切 ZGC 后部分事件没了
解决:升级到 JDK 21+(JFR 事件完整覆盖 ZGC)
配置事件:
-XX:StartFlightRecording=settings=profile,filename=app.jfr,duration=300s

JVM 监控

# Micrometer + Prometheus
management:
  endpoints:
    web:
      exposure:
        include: prometheus, health, metrics
  metrics:
    tags:
      application: ${spring.application.name}
    distribution:
      percentiles-histogram:
        http.server.requests: true
      percentiles:
        http.server.requests: 0.5, 0.95, 0.99

# 关键 metrics
jvm_memory_used_bytes{area="heap"}
jvm_memory_committed_bytes{area="heap"}
jvm_memory_max_bytes{area="heap"}
jvm_gc_pause_seconds_count
jvm_gc_pause_seconds_sum
jvm_gc_pause_seconds_max
jvm_gc_concurrent_phase_time_seconds
jvm_threads_live_threads
jvm_threads_daemon_threads
# 告警规则
groups:
  - name: jvm
    rules:
      - alert: JVMHeapHigh
        expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85
        for: 5m
        labels: { severity: warning }

      - alert: JVMGCPauseTooLong
        expr: jvm_gc_pause_seconds_max > 0.5
        for: 1m
        labels: { severity: critical }

      - alert: JVMFullGCFrequent
        expr: rate(jvm_gc_pause_seconds_count{action="end of major GC"}[5m]) > 0.1
        for: 5m
        labels: { severity: critical }

      - alert: JVMThreadsHigh
        expr: jvm_threads_live_threads > 1000
        for: 5m
        labels: { severity: warning }

实战调优方法论

第 1 步:开 GC log + dump
JAVA_OPTS="-Xlog:gc*:file=gc.log -XX:+HeapDumpOnOutOfMemoryError"

第 2 步:看现象
- 频率:Young GC 多少次/秒?Full GC 有没有?
- 暂停:平均、p99
- 堆使用趋势:有没有泄漏(老年代持续涨)
- 吞吐量:应用时间占比

第 3 步:用工具分析
- GCeasy.io:gc.log 可视化
- VisualVM / JConsole:实时监控
- JProfiler / YourKit:深度分析
- MAT:heap dump 分析

第 4 步:针对症状调优
- Young GC 频繁 → 加大年轻代 -Xmn 或 -XX:G1NewSizePercent
- Full GC 多 → 老年代设置 / 大对象 / 内存泄漏
- 暂停长 → 切 ZGC 或 Shenandoah
- 吞吐量低 → 调 ParallelGCThreads / ConcGCThreads

第 5 步:压测验证
不要在生产乱调,先在压测环境验证
压测工具:JMeter / Gatling / wrk2

不同场景的 GC 选择

场景                                    推荐 GC
================================================
小堆 (< 4G) 通用                          G1
大堆 (4-32G) 通用                        G1 或 ZGC
超大堆 (> 32G)                           ZGC
低延迟要求 (p99 < 50ms)                  ZGC / Shenandoah
吞吐量优先(批处理)                       Parallel GC
JDK 21+ + 高并发                         ZGC (Generational)
内存敏感(K8s 小 Pod)                    G1(ZGC 多占 20-30% 内存)
JDK 8(legacy)                            G1(放弃 CMS)
压测 / benchmark                          Epsilon(不 GC,只压性能上限)

切 ZGC 后效果

指标               G1 调优后    ZGC          变化
==========================================
GC 频率(总)       1/10s        1/30s        -67%
GC 暂停 p50        50ms         1ms          -98%
GC 暂停 p99        80ms         3ms          -96%
Full GC            无           无           -
业务 p99 延迟      120ms        45ms         -63%
业务 p999 延迟     500ms        80ms         -84%
内存占用           13G          18G          +38%
CPU(GC 部分)      8%           12%          +50%

权衡:ZGC 用更多内存和 CPU 换更短的暂停
对延迟敏感的业务,这个权衡很划算

避坑清单

  1. 不要盲目选 GC,先看业务对延迟还是吞吐量的偏好
  2. GC 日志必须开,出问题时 gc.log 是第一手资料
  3. HeapDumpOnOutOfMemoryError 一定开,OOM 时有 dump 才能分析
  4. G1 大堆(> 16G)暂停时间不稳定,可以考虑 ZGC
  5. ZGC 多占 20-30% 内存,K8s Pod 内存 limits 要相应调大
  6. ZGC 几乎不用调参,简单几个就够
  7. 大对象(> region/2)避免,G1 有 Humongous 问题
  8. JDK 21 + 分代 ZGC 是当前最佳选择(2024)
  9. 压测验证,不要在生产乱调
  10. 监控 GC pause / heap usage / Full GC 频率,告警阈值要合理

总结

JVM GC 调优是 Java 服务运维的核心技能。G1 是通用最优解(JDK 9+ 默认),但堆超过 16G 后暂停时间难控制。ZGC 是新一代低延迟 GC,JDK 21 的分代 ZGC 进一步降低了内存开销,p99 暂停可以做到亚毫秒。这次从 G1 切 ZGC 让我们的 p99 延迟降了 63%,代价是 38% 内存增加,业务可以接受。最大的认知改变:GC 不是黑盒,看懂 gc.log + 用对工具,大部分问题都能定位。未来 Project Loom + ZGC 的组合会让 Java 在高并发场景重新有竞争力。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Node.js MySQL 连接池打满事故复盘:6 大坑和真实修法

2026-5-19 12:29:17

技术教程

pandas 上不动了:Polars + DuckDB 重写 5000w 行漏斗实录

2026-5-19 12:34:29

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索