04-JVM调优(二)
本讲聚焦线上 JVM 问题排查工具、GC 日志分析与典型故障案例。
线上排查工具箱
JDK 自带工具
| 工具 | 用途 |
|---|---|
| jps | 查看 Java 进程 |
| jstat | 监控 GC、类加载、编译 |
| jstack | 导出线程栈,定位死锁、阻塞 |
| jmap | 导出堆 dump、查看对象直方图 |
| jhat | 分析 dump 文件(已废弃) |
| jcmd | 统一命令入口(JDK 8+) |
命令示例
bash
# 查看 GC 概况
jstat -gcutil <pid> 1000 10
# 导出堆 dump
jmap -dump:format=b,file=heap.hprof <pid>
# 查看对象直方图
jmap -histo <pid> | head -20
# 导出线程栈
jstack <pid> > thread.txt
# 检测死锁
jstack -l <pid> | grep -A 30 "Found .* deadlock"
# 统一命令
jcmd <pid> help
jcmd <pid> Thread.print
jcmd <pid> GC.heap_info
jcmd <pid> VM.system_propertiesjstat 关键字段
text
S0 S1 E O M YGC YGCT FGC FGCT GCT
0.00 85.42 0.00 62.31 95.4 23 1.234 3 0.892 2.126解读:
- S0/S1/E:Survivor0/1、Eden 使用率
- O:老年代使用率
- M:Metaspace 使用率
- YGC/YGCT:Young GC 次数 / 总耗时
- FGC/FGCT:Full GC 次数 / 总耗时
Arthas 阿里巴巴诊断工具
安装与启动
bash
# 下载
curl -O https://arthas.aliyun.com/arthas-boot.jar
# 启动
java -jar arthas-boot.jar <pid>高频命令
bash
# 查看方法调用链
trace com.example.UserService getUserById
# 查看方法入参返回值
watch com.example.UserService getUserById '{params, returnObj}' -x 2
# 反编译类,确认线上代码版本
jad com.example.UserService
# 动态修改日志级别
logger --name ROOT --level DEBUG
# 查看 JVM 信息
dashboard
jvm
thread -n 5 # CPU 最高的 5 个线程
thread -b # 查找阻塞其他线程的线程
# 火焰图
profiler start
profiler stop --format html实战:定位 CPU 飙高
text
1. dashboard 查看 CPU 占比
2. thread -n 5 找出 CPU 最高的线程
3. thread <id> 查看线程栈
4. trace 定位到方法级别
5. watch 查看实际入参、返回值
6. 修复代码,发布回滚GC 日志分析
G1 GC 日志样例
[info][gc] GC(42) Pause Young (Normal) (G1 Evacuation Pause)
1150M->850M(2048M) 23.456ms
[info][gc] GC(43) Pause Full (G1 Compaction Pause)
1.8G->1.6G(2G) 345.123ms字段解读:
- 类型:Young / Mixed / Full
- 原因:Evacuation Pause、Metadata GC Threshold、Allocation Failure
- 堆变化:使用量从 X 变为 Y(总容量)
- 耗时:Pause 时间,应控制在 200ms 以内
分析工具
- GCViewer: 开源桌面工具,可视化 GC 日志
- gceasy.io: 在线分析,给出停顿、吞吐量、内存建议
- Eclipse MAT: 分析 heap dump,定位内存泄漏
Eclipse MAT 实战
text
1. jmap -dump 导出 hprof
2. MAT 打开,运行 Histogram 查看对象统计
3. 查看 Dominator Tree,找占内存最大的对象
4. 点击 Path To GC Roots,定位泄漏源
5. 查看 with incoming references,分析引用链常用查询:
SELECT * FROM java.util.HashMap$Node WHERE size > 100000
SELECT thread.name, thread.contextClassLoader FROM java.lang.Thread thread典型故障案例
案例 1:频繁 Full GC,CPU 飙高
现象:Full GC 每分钟 5 次,应用响应变慢。
排查步骤:
bash
# 1. 查看对象直方图
jmap -histo:live <pid> | head -20
# 发现 byte[] 占用 1.5G
# 2. Arthas trace 定位
trace com.example.CacheService put
# 发现 put 方法每次 new byte[1024*1024]
# 3. 查看代码,确认是 ThreadLocal 中残留引用修复:将 ThreadLocal 中缓存清理,或改用弱引用。
案例 2:OOM Java heap space
现象:服务启动 30 分钟后 OOM 退出。
排查步骤:
bash
# 1. 查看启动参数
jcmd <pid> VM.flags | grep HeapDumpOnOutOfMemoryError
# 2. 用 dump 文件分析
jmap -dump:format=b,file=oom.hprof <pid>
# 3. MAT 分析
# Dominator Tree 显示 ArrayList 占 2G
# Path To GC Roots 显示是 static 字段持有修复:限制集合大小,使用 LRU 淘汰(如 Guava Cache、Caffeine)。
案例 3:线程死锁,请求超时
现象:接口 QPS 下降,部分请求 30s 超时。
排查步骤:
bash
# 1. Arthas 查看线程状态
thread -b
# Found one Java-level deadlock: thread-A waiting to lock monitor 0x...
# locked by thread-B
# 2. 查看线程栈
thread <id>
# thread-A 持有 lock1,请求 lock2
# thread-B 持有 lock2,请求 lock1修复:统一加锁顺序;改用 ReentrantLock.tryLock(timeout)。
案例 4:Metaspace OOM
现象:抛出 OutOfMemoryError: Metaspace。
排查思路:
- 动态类加载过多(如反射、CGLIB、Groovy)
- 频繁 ClassLoader 创建
- 代码 hot reload(如 JRebel)
排查命令:
bash
# 查看 Metaspace 使用量
jcmd <pid> VM.metaspace
# 找出实例最多的 ClassLoader
jmap -clstats <pid>
# 查看 class 数量前 20
jcmd <pid> GC.class_histogram | head -30修复:
- 升级 MetaspaceSize 上限:
-XX:MaxMetaspaceSize=512m - 排查反射、动态代理是否泄露
- 重启服务,启用类加载监控
生产调优 Checklist
[ ] JVM 参数与监控告警
- [ ]
-Xms与-Xmx设为相同,避免抖动 - [ ]
-Xmn设置合适的新生代比例 - [ ] 启用
HeapDumpOnOutOfMemoryError,配置 dump 路径 - [ ] 启用 GC 日志,写入文件并滚动
- [ ]
[ ] 收集器选型
- [ ] 堆 < 4G:Parallel Scavenge + Parallel Old(吞吐优先)
- [ ] 堆 4G~32G:G1(停顿可控)
- [ ] 堆 > 32G 或低延迟:ZGC
[ ] 监控告警
- [ ] Prometheus + Grafana 采集 JVM 指标
- [ ] Full GC 次数、YGC 耗时、堆占用告警
- [ ] 线程数、Metaspace 监控
[ ] 故障预案
- [ ] 预案:CPU 飙高用 Arthas trace
- [ ] 预案:OOM 用 dump 文件分析
- [ ] 预案:死锁用 jstack / thread -b