04. JVM 性能调优与 GC 优化

系统对比 G1/ZGC/Shenandoah 垃圾回收器,掌握 GC 日志分析、JVM 参数调优与内存问题排查方法

JVM 调优不是堆砌参数的艺术,而是在吞吐量、延迟与内存占用之间找到业务场景的最优平衡点。本文从垃圾回收器原理出发,结合实战案例,建立系统化的调优方法论。

1. 调优三元论

         吞吐量 (Throughput)
              △
             /|\
            / | \           三者不可兼得
           /  |  \          只能根据场景侧重优化
          /   |   \
         /────┼────\
  延迟  ←─────┼─────→ 内存占用
  (Latency)   |
场景优先级推荐回收器
批处理/大数据吞吐量 > 延迟G1 / ParallelGC
Web 服务/API延迟 < 100msG1 / ZGC
交易/支付系统延迟 < 10msZGC / Shenandoah
嵌入式/容器内存占用优先SerialGC / Epsilon

2. 垃圾回收器演进

JDK 1.3 ── Serial + Serial Old
    │
JDK 1.4 ── Parallel Scavenge + Parallel Old (吞吐量优先)
    │
JDK 5  ── CMS (低延迟,已废弃)
    │
JDK 7  ── G1 (分区回收)
    │
JDK 11 ── ZGC (亚毫秒级,<10ms)
    │
JDK 15 ── Shenandoah (红帽贡献,并发压缩)
    │
JDK 21 ── Generational ZGC, EA Liberica Prime (Continuous Concurrent Marking)

3. G1 GC 深度解析

3.1 设计思想:分区回收 (Region-Based)

┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ E   │ E   │ S   │ S   │ O   │ O   │ O   │ H   │
│     │     │     │     │     │     │     │ 1M  │  ← Humongous (大对象)
│ 1M  │ 1M  │ 1M  │ 1M  │ 1M  │ 1M  │ 1M  │     │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
 Eden   Eden  Survivor     Old            Humongous

-XX:G1HeapRegionSize=1m (默认根据堆大小自动计算)

3.2 G1 回收过程

Young GC(暂停所有线程,STW)
  → 并行复制 Eden + Survivor → 新 Survivor 或 Old
    
Mixed GC(并发标记 + 筛选回收):
  1. 初始标记 (Initial Mark)       — STW,极短
  2. 并发标记 (Concurrent Mark)   — 与用户线程并发
  3. 最终标记 (Remark)            — STW
  4. 筛选回收 (Cleanup)           — STW,只回收价值最高的 Region
  5. 并发清理 (Concurrent Cleanup) — 清理空 Region

Full GC(单线程 STW,避免):堆空间不足时触发,Serial Old 回退

3.3 G1 关键参数

# 目标最大暂停时间(默认 200ms)
-XX:MaxGCPauseMillis=200

# 每次 Mixed GC 回收老年代 Region 数上限
-XX:G1MixedGCCountTarget=8

# 触发全局并发标记的老年代占用阈值(默认 45%)
-XX:InitiatingHeapOccupancyPercent=45

# 回收停顿预测模型置信度
-XX:G1ConfidencePercent=50

# 避免大对象分配断裂
-XX:G1HeapWastePercent=5

4. ZGC:低延迟革命

4.1 设计目标

  • 亚毫秒级 STW(通常 < 1ms)
  • 堆大小无关性:10MB ~ 10TB 堆暂停时间相同
  • 并发整理:回收过程与用户线程并行

4.2 核心技术:染色指针 (Colored Pointers)

64 位指针结构(Linux x86_64,使用 4 位元数据):

63 ────────────────── 47 ── 43 ── 42 ── 41 ── 40 ── 39 ──── 0
│   未使用(17位)       │ Final │ Remap │ Mark1 │ Mark0 │  实际地址(42位)  │
│                     │ izable│       │       │       │              │
└─────────────────────┴───────┴───────┴───────┴───────┴──────────────┘
           ZGC 元数据位      →     影响指针的染色状态

三色标记

  • Marked0 / Marked1:并发标记,交替使用
  • Remapped:已重映射到新地址
  • Finalizable:仅 Finalizer 对象可达

4.3 ZGC 回收周期

1. 暂停标记起始 (Pause Mark Start)        STW ~0.05ms
     └─ 根枚举(线程栈、寄存器、全局变量)
     
2. 并发标记 (Concurrent Mark)             并发
     └─ 遍历对象图,染色指针标记可达
     
3. 暂停标记结束 (Pause Mark End)           STW ~0.05ms
     └─ 处理弱引用、类卸载
     
4. 并发预备重分配 (Concurrent Prepare)      并发
     └─ 选择需要清理的 Region
     
5. 暂停重分配起始 (Pause Relocate Start)    STW ~0.05ms
     └─ 根枚举 + 设置转发表
     
6. 并发重分配 (Concurrent Relocate)         并发
     └─ 移动存活对象,更新引用(读屏障拦截)
     
7. 并发重映射 (Concurrent Remap)            并发
     └─ 清理旧地址引用,转移表回收

4.4 读屏障 (Load Barrier)

Object obj = field;  // 字节码: getfield

// 伪代码:CPU 自动插入,无感知
if (obj & bad_mask) {   // 指针染色位不正确
    obj = slow_path(obj); // 修复指针,可能触发重映射
}

性能代价:读屏障成本约 4% CPU 开销,但换来极低延迟。

4.5 ZGC 参数

# JDK 21+:分代 ZGC(推荐)
-XX:+UseZGC
-XX:+ZGenerational         # 分代模式(JDK 21 默认开启)

# 关键参数
-XX:ZCollectionInterval=5  # 强制 GC 间隔(seconds)
-XX:ZAllocationSpikeTolerance=2  # 分配速率容忍倍数
-XX:ZProactive=true        # 主动触发 GC

# JDK 15-20:非分代 ZGC
-XX:+UseZGC
-XX:MaxGCPauseMillis=<target>

5. Shenandoah

与 ZGC 类似的目标和设计,JDK 12 GA(Red Hat 开发)。

独特点

  • 使用 Brooks Pointer(转发指针)而非染色指针
  • 对象头中增加一个 forwarding 指针字段
  • 读操作 O(1) 直接获取正确地址

适用场景:与 ZGC 几乎重叠,优先看 JDK 发行版支持(OpenJDK vs. Red Hat OpenJDK)。

6. GC 日志分析

6.1 JDK 9+ 统一日志格式

-Xlog:gc*:file=gc.log:time,uptime,pid,tid,level,tags:filecount=10,filesize=100m

6.2 日志解读:G1

[2023-01-15T10:23:45.123+0800][gc,start    ] GC(42) Pause Young (Normal) (G1 Evacuation Pause)
[2023-01-15T10:23:45.124+0800][gc,task     ] GC(42) Using 8 workers of 8 for evacuation
[...]
[2023-01-15T10:23:45.145+0800][gc,heap     ] GC(42) Eden regions: 24->0(24)
[2023-01-15T10:23:45.145+0800][gc,heap     ] GC(42) Survivor regions: 3->4(4)
[2023-01-15T10:23:45.145+0800][gc,heap     ] GC(42) Old regions: 45->45
[2023-01-15T10:23:45.145+0800][gc,heap     ] GC(42) Humongous regions: 1->1
[2023-01-15T10:23:45.145+0800][gc,metaspace] GC(42) Metaspace: 128M->128M(256M)
[2023-01-15T10:23:45.145+0800][gc          ] GC(42) Pause Young (Normal) (G1 Evacuation Pause) 24M->8M(512M) 21.023ms
                                       ─────────────────────────────────────────────────────────
                                       回收前   回收后 堆容量   暂停时间

6.3 分析工具

工具用法
gcviewerGUI 分析 GC 日志,生成吞吐/暂停图表
gceasy.io在线 GC 日志分析
jconsoleJMX 实时观察堆、GC、线程
jvisualvmVisual GC 插件查看各区内存曲线

7. 常见内存问题排查

7.1 OOM 诊断矩阵

异常信息原因排查
Java heap space堆内存不足-Xmx 设置太小 / 内存泄漏
GC overhead limit exceeded98% 时间花在 GC,回收 < 2%检查 Full GC 频率 / 对象生命周期
Metaspace类元数据区溢出动态生成类(CGLIB / Groovy)
Direct buffer memory堆外内存泄漏NIO ByteBuffer / Netty 未释放
Unable to create new native thread线程数超限-Xss 太大 / ulimit -u 限制

7.2 内存泄漏排查步骤

1. -XX:+HeapDumpOnOutOfMemoryError
   ↓
2. jmap -dump:format=b,file=heap.hprof <pid>
   ↓
3. MAT (Memory Analyzer Tool) / JProfiler / VisualVM
   ↓
4. 查找 Dominator Tree 中 Retained Heap 最大的对象
   ↓
5. 追踪 GC Roots,定位泄露引用链
   ↓
6. 分析代码:缓存未设置 TTL / 监听未移除 / ThreadLocal 未清理

7.3 平响抖动案例:Young GC 频繁

现象:Young GC 每 5s 触发一次,每次 50ms,平响飙高。

分析

# gc.log 频繁出现 [gc,start] Pause Young
# Eden 区域分配速率高 → 对象生命周期极短

方案

  1. 增大 Eden(-Xmn-XX:G1NewSizePercent
  2. 优化代码:避免短生命周期大对象
  3. 对象池化(Netty ByteBuf、线程本地缓存)

7.4 平响飙升案例:Full GC

现象:每小时 Full GC,每次 3s。

分析

# [gc              ] GC(XX) Pause Full (System.gc())
# 或元空间 / 老年代晋升过多

方案

  1. 排查 System.gc() 调用(-XX:+DisableExplicitGC 或使用 -XX:+ExplicitGCInvokesConcurrent)
  2. 增加堆大小,优化对象晋升阈值
  3. 检查大对象直接进入老年代(-XX:PretenureSizeThreshold

8. 容器环境下的 JVM

# JDK 8u191+ / JDK 9+ 原生支持容器内存限制
-XX:+UseContainerSupport  # 默认开启(JDK 10+)

# 堆占容器内存的比例(默认 25%)
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0

# JDK 8u191 之前的版本:
# 设置堆大小,不要超过容器 limit(否则被 OOM Killer)
-Xmx512m -Xms512m

# Kubernetes 资源请求示例
resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "2Gi"      # JVM 读取 cgroup limit
    cpu: "2000m"

cgroup v1 vs v2 JDK 限制读取:JDK 15+ 完整支持 cgroup v2。容器平台的 JVM 版本不得低于此基线。

9. 调优清单

# 1. 选择目标
-XX:MaxGCPauseMillis=50

# 2. 选择回收器
-XX:+UseZGC                # 延迟敏感
-XX:+UseG1GC               # 均衡
-XX:+UseParallelGC         # 吞吐量

# 3. 堆内存
-Xms4g -Xmx4g              # 固定堆,避免动态调整开销
-Xmn1g                     # 年轻代

# 4. GC 日志
-Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=10,filesize=100m

# 5. OOM 诊断
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/

# 6. 调试(仅开发环境)
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintFlagsFinal       # 输出所有 JVM 参数默认值

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析