JVM 调优
面试提问
"线上服务出现 Full GC 频繁,CPU 飙高,你如何排查和调优?"
核心概念
JVM 调优的本质是在有限资源下,让应用获得最佳性能。调优不是盲目调整参数,而是基于监控数据做出的科学决策。
调优目标
| 指标 | 说明 | 目标值 |
|---|---|---|
| 吞吐量 | 单位时间处理的请求数 | 越高越好 |
| 延迟 | 请求响应时间 | 越低越好 |
| 内存占用 | JVM 使用内存 | 在可控范围内 |
| GC 停顿 | STW 时间 | 尽量短 |
⚠️ 吞吐量和延迟往往不可兼得,需要根据业务场景取舍。
JVM 参数分类
1. 内存参数
-Xms 初始堆大小(建议与 Xmx 相同)
-Xmx 最大堆大小
-Xmn 年轻代大小
-Xss 线程栈大小
-XX:NewRatio 年轻代与老年代比例(默认 1:2)
-XX:SurvivorRatio Eden 与 Survivor 比例(默认 8:1:1)
-XX:MetaspaceSize 元空间初始大小
-XX:MaxMetaspaceSize 元空间最大大小2. GC 参数
-XX:+UseSerialGC 使用 Serial 收集器
-XX:+UseParallelGC 使用 Parallel 收集器(JDK8 默认)
-XX:+UseConcMarkSweepGC 使用 CMS 收集器
-XX:+UseG1GC 使用 G1 收集器(JDK9+ 默认)
-XX:+UseZGC 使用 ZGC(JDK15+)3. GC 日志参数
# JDK8 及之前
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log
# JDK9+
-Xlog:gc*:file=gc.log:time,level,tags调优实战步骤
第一步:监控发现问题
监控指标:
- GC 频率(Young GC / Full GC 次数)
- GC 耗时(单次 STW 时间)
- 内存使用趋势(是否泄漏)
- CPU 使用率
工具:
jstat -gcutil <pid> 1000- 实时 GC 统计jmap -heap <pid>- 堆内存信息jvisualvm/Arthas- 可视化监控GCViewer- GC 日志分析
第二步:分析定位原因
常见问题:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| Young GC 频繁 | 年轻代太小 | 增大年轻代 |
| Full GC 频繁 | 老年代填满 / 元空间满 | 检查内存泄漏 |
| GC 耗时长 | 堆太大 / 大对象多 | 调整堆大小 |
| CPU 飙高 | GC 线程竞争 / 业务问题 | 分析线程栈 |
第三步:调整优化
调优原则:
- 先调整内存分配,再更换 GC 收集器
- 一次只调一个参数,对比效果
- 记录基准数据,量化优化效果
典型场景调优
场景一:Young GC 频繁
现象:每秒多次 Young GC,但 Full GC 很少
分析:年轻代太小,对象过早晋升到老年代
调优:
# 增大年轻代(直接指定或调整比例)
-Xmn512m
# 或
-XX:NewRatio=1 # 年轻代:老年代 = 1:1场景二:Full GC 频繁
现象:Full GC 间隔很短,系统卡顿
分析:
- 老年代空间不足
- 元空间满(加载太多类)
- 内存泄漏
调优:
# 增大堆内存
-Xms2g -Xmx2g
# 增大元空间
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
# 如果是 G1,降低触发阈值
-XX:InitiatingHeapOccupancyPercent=35 # 默认45场景三:大对象导致 GC 慢
现象:每次 GC 耗时很长,有大量大对象
分析:大对象直接进入老年代,或 Humongous 区域
调优:
# 设置大对象阈值(Serial/Parallel)
-XX:PretenureSizeThreshold=1m
# G1 的 Humongous 对象阈值(Region 的 50%)
# G1 默认 Region 大小 = 堆大小 / 2048
-XX:G1HeapRegionSize=16m场景四:延迟敏感型应用
要求:GC 停顿控制在 50ms 以内
调优:
# 使用 G1,设置停顿目标
-XX:+UseG1GC
-XX:MaxGCPauseMillis=50
# 或使用 ZGC(JDK15+)
-XX:+UseZGC
-XX:ZCollectionInterval=5 # 主动 GC 间隔常用调优参数速查
堆内存配置
| 参数 | 默认值 | 建议 |
|---|---|---|
-Xms | 物理内存 1/64 | 与 -Xmx 相同,避免动态扩容 |
-Xmx | 物理内存 1/4 | 不超过物理内存 80% |
-Xmn | 堆的 1/3 | 高频创建对象可增大 |
GC 收集器选择
| JDK 版本 | 默认 GC | 推荐场景 |
|---|---|---|
| JDK 8 | Parallel | 批处理、计算密集 |
| JDK 9+ | G1 | 通用、延迟敏感 |
| JDK 15+ | ZGC | 超低延迟(<10ms) |
G1 关键参数
-XX:+UseG1GC # 启用 G1
-XX:MaxGCPauseMillis=200 # 最大停顿时间目标
-XX:G1HeapRegionSize=16m # Region 大小
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发 GC 的堆占用率
-XX:G1NewSizePercent=5 # 年轻代最小占比
-XX:G1MaxNewSizePercent=60 # 年轻代最大占比调优案例分析
案例:电商服务 Full GC 频繁
现象:
- 4G 堆内存,每 5 分钟一次 Full GC
- 每次 Full GC 停顿 2-3 秒
- 系统响应时间抖动
排查过程:
# 1. 查看 GC 统计
jstat -gcutil 12345 1000 10
# 发现老年代使用率接近 100%
# 2. 生成堆转储
jmap -dump:format=b,file=heap.hprof 12345
# 3. MAT 分析,发现大量 Order 对象无法回收问题根因:
- 订单缓存未设置过期时间
- 订单对象持续累积
解决方案:
- 业务层面:给缓存设置过期时间
- JVM 层面:增大堆内存 + 切换 G1
# 调整后参数
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200优化效果:
- Full GC 频率:5 分钟 → 2 小时
- GC 停顿:2-3 秒 → 100ms 以内
调优误区
❌ 误区一:调优能解决所有问题
调优只是锦上添花,代码问题才是根本:
- 内存泄漏 → 排查代码
- 大对象频繁创建 → 优化业务逻辑
- 死循环/阻塞 → 定位具体代码
❌ 误区二:堆越大越好
堆过大会导致:
- GC 扫描时间变长
- 内存浪费
- 可能触发 swap,性能骤降
建议:堆大小不超过物理内存的 80%
❌ 误区三:盲目复制参数
不同应用的特点不同:
- 计算密集 vs IO 密集
- 吞吐量优先 vs 延迟优先
- 小堆 vs 大堆
建议:基于监控数据,针对性调优
面试要点总结
| 问题 | 答案要点 |
|---|---|
| 如何发现 GC 问题? | jstat 监控、GC 日志分析、可视化工具 |
| Young GC 频繁怎么办? | 增大年轻代、调整 SurvivorRatio |
| Full GC 频繁怎么办? | 排查内存泄漏、增大堆/元空间、调整 G1 阈值 |
| 如何选择 GC 收集器? | JDK8 用 Parallel/G1,JDK9+ 优先 G1,超低延迟用 ZGC |
| 调优的核心原则? | 先监控后调优、一次一个参数、量化效果 |
相关题目
- 说一说常用的 JVM 调优参数?
- 如何排查线上 CPU 飙高问题?
- G1 和 CMS 的调优有什么区别?
- 线上服务 OOM 如何排查?