JVM 调优
问题
JVM 调优的思路是什么?常用的 JVM 参数有哪些?如何分析 GC 日志?
答案
调优目标与指标
JVM 调优的核心是在吞吐量、延迟、内存占用三者之间取得平衡:
| 指标 | 含义 | 关注场景 |
|---|---|---|
| 吞吐量 | 用户代码执行时间占总时间的比例 | 批处理、后台计算 |
| 延迟(停顿时间) | 单次 GC 导致的 STW 时间 | Web 服务、实时系统 |
| 内存占用 | 堆的使用量和增长趋势 | 资源受限环境 |
- 先做好代码层面优化:减少对象创建、避免内存泄漏
- 尽量减少 Full GC:Full GC 是性能杀手
- 不要过度调优:JVM 默认参数已经很好,只在出现问题时调优
- 用数据说话:每次调整都要有 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 secs | GC 耗时 52ms |
user=0.15 | 用户态 CPU 时间 |
real=0.05 | 实际挂钟时间 |
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 调优的一般步骤是什么?
答案:
- 确定调优目标:先明确是优化吞吐量、降低延迟还是减少内存占用
- 收集数据:开启 GC 日志,使用监控工具收集指标
- 分析瓶颈:用 GCViewer/GCEasy 分析 GC 日志,定位问题(频繁 Full GC?停顿过长?内存泄漏?)
- 制定方案:根据问题调整参数或优化代码
- 压测验证:在测试环境压测,对比调优前后数据
- 上线观察:灰度发布,持续监控
Q2: -Xms 和 -Xmx 为什么要设置成一样?
答案:
如果 -Xms < -Xmx,JVM 启动时只分配 -Xms 大小的堆内存,随着对象增多再动态扩展到 -Xmx。每次扩展都需要重新分配内存并可能触发 Full GC。设置相同可以:
- 避免动态扩缩容的开销
- 避免扩容时的 Full GC
- 启动时就获得所需内存,运行更稳定
Q3: 如何排查频繁 Full GC?
答案:
排查步骤:
- 查看 GC 日志:确认 Full GC 的触发原因(老年代满?元空间满?System.gc()?)
- 分析堆内存:
jmap -histo:live <pid>查看对象分布 - 导出 Heap Dump:
jmap -dump:live,format=b,file=heap.hprof <pid> - 用 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 参数如何确定?
答案:
- 先用默认参数运行,收集一段时间的 GC 数据
- 分析 GC 日志,关注 Full GC 频率、Young GC 耗时、堆使用率
- 容量规划:堆大小通常为活跃数据的 3
4 倍,新生代为堆的 1/31/2 - 选择合适的 GC:
- JDK 8:默认 Parallel,Web 服务可换 G1
- JDK 17+:默认 G1,大堆可用 ZGC
- 压测调整:通过压测验证参数是否合适