跳到主要内容

JVM 调优

问题

JVM 调优的思路是什么?常用的 JVM 参数有哪些?如何分析 GC 日志?

答案

调优目标与指标

JVM 调优的核心是在吞吐量延迟内存占用三者之间取得平衡:

指标含义关注场景
吞吐量用户代码执行时间占总时间的比例批处理、后台计算
延迟(停顿时间)单次 GC 导致的 STW 时间Web 服务、实时系统
内存占用堆的使用量和增长趋势资源受限环境
调优原则
  1. 先做好代码层面优化:减少对象创建、避免内存泄漏
  2. 尽量减少 Full GC:Full GC 是性能杀手
  3. 不要过度调优:JVM 默认参数已经很好,只在出现问题时调优
  4. 用数据说话:每次调整都要有 GC 日志和监控数据支撑

调优流程

常用 JVM 参数

内存相关

# ===== 堆内存 =====
-Xms4g # 初始堆大小(建议与 -Xmx 相同,避免动态调整)
-Xmx4g # 最大堆大小

# ===== 新生代 =====
-Xmn1g # 新生代大小(G1 中不建议手动设置)
-XX:NewRatio=2 # 老年代:新生代 = 2:1
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1

# ===== 栈 =====
-Xss512k # 线程栈大小(默认 1MB,可适当减小以支持更多线程)

# ===== 元空间 =====
-XX:MetaspaceSize=256m # 元空间初始阈值
-XX:MaxMetaspaceSize=512m # 元空间上限(建议设置,防止无限增长)

# ===== 直接内存 =====
-XX:MaxDirectMemorySize=256m

GC 相关

# ===== 选择收集器 =====
-XX:+UseG1GC # G1(JDK 9+ 默认)
-XX:+UseZGC # ZGC(JDK 11+)
-XX:+UseParallelGC # Parallel(JDK 8 默认)

# ===== G1 参数 =====
-XX:MaxGCPauseMillis=200 # G1 目标最大停顿时间(默认 200ms)
-XX:G1HeapRegionSize=8m # Region 大小(1~32MB,2 的幂)
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用率
-XX:G1MixedGCCountTarget=8 # Mixed GC 轮数
-XX:G1ReservePercent=10 # 预留内存防止 to-space 溢出

# ===== Parallel 参数 =====
-XX:GCTimeRatio=99 # GC 时间不超过 1%
-XX:+UseAdaptiveSizePolicy # 自适应策略

# ===== 通用 =====
-XX:MaxTenuringThreshold=15 # 晋升年龄阈值(最大 15)
-XX:PretenureSizeThreshold=4m # 大对象直接进老年代(仅 Serial/ParNew)

GC 日志

# ===== JDK 8 日志格式 =====
-XX:+PrintGCDetails # 详细 GC 日志
-XX:+PrintGCDateStamps # GC 发生的时间戳
-XX:+PrintGCTimeStamps # GC 相对启动的时间
-XX:+PrintHeapAtGC # GC 前后堆信息
-Xloggc:/path/to/gc.log # GC 日志输出路径
-XX:+UseGCLogFileRotation # 日志滚动
-XX:NumberOfGCLogFiles=5 # 保留日志文件数
-XX:GCLogFileSize=20m # 单个日志文件大小

# ===== JDK 9+ 统一日志框架 =====
-Xlog:gc*:file=/path/to/gc.log:time,uptime,level,tags:filecount=5,filesize=20m

诊断参数

# ===== OOM 时自动导出堆转储 =====
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof

# ===== 其他 =====
-XX:+PrintFlagsFinal # 打印所有 JVM 参数的最终值
-XX:+DisableExplicitGC # 禁止 System.gc()(慎用,影响 NIO 堆外内存回收)

GC 日志分析

JDK 8 GC 日志示例

// Minor GC 日志
2024-01-15T10:30:45.123+0800: 12.456:
[GC (Allocation Failure)
[PSYoungGen: 524288K->65536K(611840K)]
1048576K->589824K(2010112K), 0.0523456 secs]
[Times: user=0.15 sys=0.01, real=0.05 secs]

各部分含义:

字段含义
GC (Allocation Failure)Minor GC,触发原因是分配失败
PSYoungGen: 524288K->65536K(611840K)新生代:GC 前 512MB → GC 后 64MB(总 598MB)
1048576K->589824K(2010112K)整堆:GC 前 1GB → GC 后 576MB(总 1.9GB)
0.0523456 secsGC 耗时 52ms
user=0.15用户态 CPU 时间
real=0.05实际挂钟时间
real < user 说明多线程并行 GC

user 是所有 GC 线程的 CPU 时间总和,real 是实际耗时。如果 user > real,说明 GC 使用了多线程并行处理。

G1 日志分析关注点

# 使用工具分析
# 1. GCViewer(开源,GUI)
java -jar gcviewer-x.x.jar gc.log

# 2. GCEasy(在线分析)
# 上传 gc.log 到 https://gceasy.io

# 3. JDK 自带
jstat -gcutil <pid> 1000 # 每秒打印一次 GC 统计

G1 日志的关键信息:

关注点正常范围告警信号
Young GC 频率几秒到几十秒一次< 1 秒一次(对象创建过快)
Young GC 耗时< 100ms> 200ms(新生代过大或存活率高)
Mixed GC 频率较少频繁 Mixed GC(老年代增长快)
Full GC没有出现 Full GC(需要调优)
to-space exhausted不出现出现(Survivor/老年代空间不足)

常见调优场景

场景 1:频繁 Minor GC

症状:Young GC 过于频繁(每秒多次),应用响应变慢

# 原因:新生代太小,或者对象创建过多
# 方案:
-Xmn1g # 增大新生代
-XX:SurvivorRatio=6 # 调整 Eden 和 Survivor 比例
# 同时检查代码是否有不必要的对象创建

场景 2:频繁 Full GC

症状:频繁 Full GC 导致长时间 STW

# 常见原因及对策:
# 1. 内存泄漏 → 用 MAT 分析 heap dump
# 2. 大对象直接进老年代 → 增大新生代或调整 PretenureSizeThreshold
# 3. 元空间不足 → 增大 MaxMetaspaceSize
# 4. System.gc() → 添加 -XX:+DisableExplicitGC
# 5. 老年代空间不足 → 增大堆或优化对象生命周期

场景 3:GC 停顿时间过长

# G1 调优
-XX:MaxGCPauseMillis=100 # 降低目标停顿时间
-XX:G1HeapRegionSize=16m # 增大 Region(减少 Region 数量)
-XX:ConcGCThreads=4 # 增加并发 GC 线程

# 考虑切换到 ZGC(JDK 17+)
-XX:+UseZGC

场景 4:堆外内存增长

# 监控堆外内存
-XX:NativeMemoryTracking=detail
jcmd <pid> VM.native_memory detail

# 限制直接内存
-XX:MaxDirectMemorySize=256m
# 检查 NIO ByteBuffer 是否正确释放

生产环境推荐配置

中小型 Web 应用(JDK 17+,4核8G)

-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump.hprof
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20m

大堆低延迟服务(JDK 21+,8核16G)

-Xms12g -Xmx12g
-XX:+UseZGC
-XX:+ZGenerational
-XX:SoftMaxHeapSize=10g
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump.hprof
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20m

常见面试问题

Q1: JVM 调优的一般步骤是什么?

答案

  1. 确定调优目标:先明确是优化吞吐量、降低延迟还是减少内存占用
  2. 收集数据:开启 GC 日志,使用监控工具收集指标
  3. 分析瓶颈:用 GCViewer/GCEasy 分析 GC 日志,定位问题(频繁 Full GC?停顿过长?内存泄漏?)
  4. 制定方案:根据问题调整参数或优化代码
  5. 压测验证:在测试环境压测,对比调优前后数据
  6. 上线观察:灰度发布,持续监控

Q2: -Xms 和 -Xmx 为什么要设置成一样?

答案

如果 -Xms < -Xmx,JVM 启动时只分配 -Xms 大小的堆内存,随着对象增多再动态扩展到 -Xmx。每次扩展都需要重新分配内存并可能触发 Full GC。设置相同可以:

  1. 避免动态扩缩容的开销
  2. 避免扩容时的 Full GC
  3. 启动时就获得所需内存,运行更稳定

Q3: 如何排查频繁 Full GC?

答案

排查步骤:

  1. 查看 GC 日志:确认 Full GC 的触发原因(老年代满?元空间满?System.gc()?)
  2. 分析堆内存jmap -histo:live <pid> 查看对象分布
  3. 导出 Heap Dumpjmap -dump:live,format=b,file=heap.hprof <pid>
  4. 用 MAT/VisualVM 分析:查找大对象、泄漏嫌疑对象

常见原因:

  • 内存泄漏(对象一直被引用无法回收)
  • 大对象过多(直接进入老年代)
  • 代码中调用 System.gc()
  • 老年代空间设置过小

详细排查流程参考 OOM 排查

Q4: 什么是 GC 日志中的 Allocation Failure?

答案

Allocation Failure 是最常见的 Minor GC 触发原因,表示在 Eden 区分配新对象时空间不足,需要触发一次 Young GC 来回收空间。这是正常现象,不用担心。

需要关注的是 GC 频率和耗时:

  • 频率过高(每秒多次)→ 新生代太小或对象创建过快
  • 耗时过长(> 200ms)→ 新生代过大或存活率太高

Q5: -XX:+DisableExplicitGC 有什么影响?

答案

该参数禁止 System.gc() 触发的 Full GC。好处是避免代码中不必要的 Full GC 调用。

但要注意:NIO 使用的堆外内存(DirectByteBuffer)依赖 System.gc() 触发的 Full GC 来清理。如果禁用了 System.gc() 且堆外内存较大,可能导致堆外内存 OOM。

折中方案:使用 -XX:+ExplicitGCInvokesConcurrent,让 System.gc() 触发并发 GC 而不是 Full GC。

Q6: G1 的 -XX:MaxGCPauseMillis 能保证一定达到吗?

答案

不能保证,这是一个目标值,不是承诺值。G1 会尽可能满足这个目标,但在以下情况下可能超出:

  • 堆使用率过高
  • 大对象(Humongous)分配频繁
  • Mixed GC 赶不上分配速度
  • 触发了 Full GC(退化为单线程 Serial)

设置过低的值(如 10ms)可能导致 G1 每次回收太少的 Region,反而触发更频繁的 GC。

Q7: 实际项目中 JVM 参数如何确定?

答案

  1. 先用默认参数运行,收集一段时间的 GC 数据
  2. 分析 GC 日志,关注 Full GC 频率、Young GC 耗时、堆使用率
  3. 容量规划:堆大小通常为活跃数据的 34 倍,新生代为堆的 1/31/2
  4. 选择合适的 GC
    • JDK 8:默认 Parallel,Web 服务可换 G1
    • JDK 17+:默认 G1,大堆可用 ZGC
  5. 压测调整:通过压测验证参数是否合适

相关链接