Skip to content

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_properties

jstat 关键字段

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