前言
前面三篇都是JVM理论知识,理论最终目的就是为了线上故障排查、JVM调优。 Java后端线上最常遇到三类噩梦级问题:
- 服务器CPU持续飙高,负载居高不下,接口响应很慢
- 线程死锁,服务卡住、接口卡死不返回,线程堆积
- OOM内存溢出,程序直接崩溃重启,频繁宕机
很多同学遇到线上问题只会看日志,日志没有报错就无从下手。 本篇分为两大部分:第一部分JDK自带工具(无需额外安装,服务器自带);第二部分Arthas阿里诊断神器实战;每一类故障都给一套标准化排查流程、命令、定位思路。
前置回顾:STW、GC、堆、虚拟机栈、线程栈信息,是看懂dump文件的基础。
一、前置说明:JDK自带工具包
在JDK/bin目录下,自带一系列诊断工具,不需要在服务里引入依赖,生产环境优先使用(很多公司禁止随便上传Arthas)。
| 工具 | 作用 |
|---|---|
| jps | 查看Java进程PID,最基础入口 |
| jstat | GC统计监控,看GC频率、耗时、新生代老年代变化 |
| jstack | 打印线程栈快照,排查死锁、CPU高、线程阻塞 |
| jmap | 堆快照dump,查看对象内存占用,排查OOM、内存泄漏 |
| jhat | 解析hprof堆快照(弱,生产一般不用,改用MAT) |
| jvisualvm | 图形化监控工具,本地调试很方便 |
重要提醒:所有dump、jstack操作,都会触发短暂STW;线上不要频繁执行,拿到一次有效快照就先分析,不要反复打快照。
1.1 jps 先拿到进程PID(所有排查第一步)
# 列出所有java进程PID、主类名 jps -l # 输出示例 12568 com.xxx.Application 23410 org.apache.tomcat.startup.Bootstrap拿到PID=12568,后续所有命令都带上这个PID。
二、场景1:CPU 100% 飙高排查(最高频)
排查思路
- top 命令找到占用CPU最高的Java进程PID
- top -H -p 进程PID,找到这个进程内部CPU最高的线程TID(十进制)
- 将十进制TID转为16进制
- jstack打印进程线程栈,根据16进制线程ID定位线程栈,看这段线程在执行什么代码
- 定位代码:死循环、大量计算、频繁GC、正则回溯、大循环等
完整实操命令
# 1、查看服务器整体CPU,找到java进程PID top # 2、查看该进程下所有线程CPU占用,找到高CPU线程tid(十进制) top -H -p 12568 # 3、十进制转16进制(例如tid=13002) printf "%x\n" 13002 # 输出:32ca # 4、导出线程栈 jstack 12568 > thread.txt # 在thread.txt中搜索 0x32ca,找到对应线程栈典型高CPU栈场景
- 看到大量业务代码循环逻辑:代码死循环、while(true)没有sleep、大数据量循环处理
- 看到
GC task thread:GC线程占满CPU →GC频繁,大概率内存泄漏,转去排查GC/OOM - 正则相关包:正则回溯灾难性回溯导致CPU跑满
Arthas方式快速定位CPU高(更简单)
# 启动arthas,attach到java进程 java -jar arthas-boot.jar # 命令:监控线程CPU,默认采样10次 thread -n 5直接输出CPU最高的5个线程栈,一步到位,省去top转16进制的繁琐。
三、场景2:线程死锁、接口卡死、线程阻塞
死锁四个必要条件(面试必背)
- 互斥条件:资源只能一个线程持有
- 请求与保持:线程拿到一个锁,再去申请另外一个锁
- 不可剥夺:锁不能被其他线程强行抢走
- 循环等待:线程A拿锁1等锁2;线程B拿锁2等锁1
满足4条就会产生死锁,线程永久卡在那里,不会释放,连接池慢慢耗尽,所有接口卡住。
jstack排查死锁
jstack 12568 > thread.txt打开文件,直接搜索关键字:Found one Java-level deadlockjstack会自动帮我们检测出Java锁死锁,输出:
- 哪些线程
- 各自持有的锁
- 正在等待哪个锁
- 对应代码行号
区分:死锁 vs 阻塞 死锁:线程互相等锁,永远不会自己恢复,必须重启
普通阻塞:等待数据库连接、http接口、锁竞争,外部资源恢复后可以自己解开
Arthas查看线程
# 查看所有线程,支持查看阻塞、死锁 thread -bthread -b专门用来定位阻塞线程,找出持有锁的线程,快速定位锁等待链路。
模拟死锁代码(方便本地复现测试)
public class DeadLockDemo { private static final Object lockA = new Object(); private static final Object lockB = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lockA) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockB) { System.out.println("线程1拿到两把锁"); } } }).start(); new Thread(() -> { synchronized (lockB) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockA) { System.out.println("线程2拿到两把锁"); } } }).start(); } }运行后jstack即可抓到死锁日志。
四、场景3:OOM内存溢出、内存泄漏(最复杂)
OOM的本质:堆内存耗尽,Full GC之后仍然没有足够内存分配新对象,JVM抛出OutOfMemoryError。
区分两个概念: OOM:内存用完了,直接报错崩溃 内存泄漏:对象本应该被回收,但持续被引用链持有,慢慢堆积,最终引发OOM
方案A:提前配置JVM参数,OOM自动dump(生产推荐)
在启动脚本增加参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/dump/oom.hprof程序OOM崩溃瞬间,自动导出堆快照,保存现场,不会丢失证据。
注意:hprof文件大小约等于-Xmx最大堆,磁盘要预留足够空间。
方案B:手动导出堆快照(服务还没崩,只是内存持续上涨)
# jmap导出堆快照 jmap -dump:format=b,file=heap.hprof 12568警告:jmap dump全堆会STW,大堆会卡顿很久,线上谨慎操作!
拿到hprof之后怎么分析?
生产服务器不适合分析大hprof,下载到本地,使用MAT(Eclipse Memory Analyzer Tool),这是行业标准工具。 MAT核心分析方向:
- Histogram直方图:看哪个类实例数量最多、占用内存最大
- Dominator Tree支配树:看谁持有这些对象,找出GC引用链,定位泄漏源头
- Leak Suspects自动报告:MAT会给出可疑内存泄漏候选
常见内存泄漏原因:
- 静态集合(static List/Map)不断add元素,永远不清空
- 线程池线程持有对象,线程不销毁,对象无法释放
- 监听器、回调没有移除,持续持有引用
- 数据库连接、IO流忘记关闭
- ThreadLocal没有remove,线程复用导致对象堆积
Arthas查看内存实时情况
# 查看堆内存使用情况,等价jstat memory # 查看对象统计 heapdump --live /tmp/heap.hprof五、jstat:持续监控GC状态(判断GC是否异常)
很多时候不是直接OOM,而是GC越来越频繁、FullGC变多,接口慢慢变慢。 jstat用来持续观察GC指标。
# 持续监控gc,每1000ms打印一次,一共打印20次 jstat -gc 12568 1000 20输出字段含义(重点):
- S0C、S1C:两个Survivor容量
- S0U、S1U:Survivor使用大小
- EC、EU:Eden容量、使用
- OC、OU:老年代容量、使用
- MC、MU:元空间
- YGC、YGCT:Minor GC次数、Minor GC总耗时
- FGC、FGCT:Full GC次数、Full GC总耗时
- GCT:GC总耗时
重点观察:
- OU(老年代使用)持续稳步上涨,YGC越来越频繁 → 大概率内存泄漏
- FGC持续上涨,每次FullGC后内存几乎不下降 → 泄漏实锤
- YGCT/FGCT单次时间很长 → STW停顿严重,需要调优收集器、堆大小
六、Arthas核心命令汇总(实战高频)
Arthas是阿里开源Java诊断工具,功能比原生命令强大很多,适合快速定位。
# 1. 查看线程(CPU高) thread -n 5 # 2. 找阻塞锁线程 thread -b # 3. 查看JVM内存状态 memory # 4. 监控方法执行耗时(定位慢接口) watch com.xxx.service.UserService queryUser '{params,returnObj,throwExp}' # 5. 查看方法调用栈 stack com.xxx.service.UserService queryUser # 6. 统计方法QPS、耗时 trace com.xxx.service.UserService queryUser # 7. 导出堆快照 heapdump --live /tmp/dump.hprof # 8. 查看类加载 sc # 9. 退出arthas stopwatch/trace/stack:适合排查接口慢,定位到底是哪一行代码、哪个第三方调用耗时久。
七、三大故障标准化排查流程图
7.1 CPU高排查流程
top找到JavaPID → top -H -p PID找到高CPU线程tid → 转16进制 → jstack搜索线程栈 → 判断死循环/GC暴涨/正则问题
7.2 死锁/线程卡死流程
接口超时堆积 → jstack导出线程栈 → 搜索Found one Java-level deadlock → 拿到锁信息和代码行 → 修改锁顺序
7.3 OOM/内存持续上涨流程
jstat观察老年代是否持续上涨 → 自动dump或者jmap导出hprof → MAT分析支配树、直方图 → 定位引用链和泄漏点
八、本篇背诵清单
- 线上CPU100%,你的排查步骤是什么?
- 怎么定位死锁?死锁四个条件?
- OOM和内存泄漏区别?如何自动dump堆快照?
- jstack、jmap、jstat各自作用?
- 拿到hprof堆文件,用什么工具分析,重点看什么?
- Arthas的thread/watch/trace/stack分别是干嘛的?
- 频繁FullGC,但是没有OOM,怎么排查?
- ThreadLocal内存泄漏原因?
九、排坑总结
- 不要频繁jstack、jmap大堆,STW会加重线上卡顿;优先jstat观察趋势
- dump文件一定要加
--live,只dump存活对象,文件更小,分析更快 - 区分GC线程CPU高 和业务代码CPU高,二者解决方案完全不同
- 死锁不会自动恢复,一旦发现死锁,只能重启服务+修复代码
- Arthas很多公司生产环境管控严格,优先掌握原生jps/jstack/jstat/jmap
小贴士
到这里整个JVM专栏四篇正文就全部结束了:运行时数据区 → 堆分代模型 → GC算法&收集器 → 线上故障排查实战。 后面你可以基于这套理论,继续拓展JVM调优实战案例,比如G1调优参数、GC日志解读。