常见问题排查
1. 这是什么
常见问题排查是把前面的 JVM 理论和工具能力真正用起来的阶段。
重点不是再学新概念,而是把问题分析变成可复用的方法。
一句话理解:
- JVM 学习的终点不是会背术语
- 而是系统出问题时能快速建立证据链
2. 为什么重要
真正的高级能力,不体现在背了多少概念,而体现在这些时刻:
- OOM 了,能不能快速知道是堆、元空间还是直接内存问题
- CPU 飙高了,能不能快速定位是哪个线程、哪段代码
- Full GC 频繁了,能不能区分是堆太小、对象模型不合理还是内存泄漏
- 类加载冲突了,能不能判断是依赖版本问题还是类加载器隔离问题
JVM 学习的价值,最终都要落到排障上。
3. 先建立直觉:排障的核心不是“先改参数”,而是先拿证据
排障最容易犯的错就是:
- 一出问题就重启
- 一出问题就改参数
- 一出问题就猜“肯定是 GC”
更稳的思路应该是:
- 先看现象
- 再收集指标
- 再用工具验证
- 最后形成结论和动作
可以把它记成:
- 现象 -> 指标 -> 工具 -> 证据 -> 结论 -> 处理
4. 核心内容
4.1 OOM 排查
先问几个关键问题:
- 是 Java 堆 OOM 吗?
- 是元空间 OOM 吗?
- 是直接内存问题吗?
- 有没有 Heap Dump?
优先收集:
- 错误类型
- JVM 启动参数
- 堆转储文件
jmap -histo- GC 日志
常见根因包括:
- 内存泄漏
- 缓存过大
- 单次请求拉起过多对象
- 长生命周期对象未释放
4.2 Full GC 频繁排查
遇到 Full GC 频繁时,不要第一反应就认为:
- “堆太小”
还可能是:
- 老年代存活对象太多
- 晋升过快
- 大对象分配异常
- 内存泄漏
- 参数配置不合理
优先看:
- GC 日志
- 堆对象分布
- 存活对象规模
- 是否存在明显热点对象类型
4.3 CPU 飙高排查
CPU 问题最常用的一条路径是:
- 先看系统层 CPU 高不高
- 找到高 CPU 进程
- 用
jstack看线程栈 - 定位热点线程在干什么
常见根因包括:
- 死循环
- 自旋重试过多
- 锁竞争严重
- 某段业务逻辑复杂度失控
4.4 死锁排查
死锁问题最经典的工具是:
jstack
典型现象:
- 请求卡死
- 线程数看着还在
- 但部分核心线程一直不推进
jstack 往往能直接告诉你:
- 哪些线程互相等待
- 锁对象是谁
- 死锁是否成立
4.5 类加载冲突排查
常见症状:
ClassNotFoundExceptionNoClassDefFoundErrorClassCastException但看起来“明明是同一个类”- 框架启动时报依赖冲突
优先思路:
- 先看类名和包名
- 再看依赖版本
- 再看类加载器
- 最后看是不是多模块 / 插件环境下的隔离问题
5. 一个通用排障流程
这套流程非常值得反复练:
- 明确现象:是慢、卡、OOM、CPU 高,还是 Full GC 频繁
- 收集基础信息:时间点、流量、版本、参数、日志
- 选择工具:线程看
jstack,堆看jmap,GC 看jstat/ GC 日志 - 建立证据链:不要只看一条信息
- 形成结论:明确根因类别
- 给出动作:调参数、修代码、改对象模型、调整线程池、优化依赖
6. 学习重点
这一章最重要的是形成这些习惯:
- 先看现象,再看指标,再下手
- 不要只盯单点,要建立整体视角
- 证据链比经验判断更重要
- JVM 问题常常和代码、对象模型、线程模型一起看
7. 常见问题
7.1 一出问题就改参数或重启
这样虽然可能暂时恢复服务,但根因会被掩盖,问题还会回来。
7.2 没有证据链,结论全靠猜
只看一条日志、一个指标,很容易误诊。
7.3 只关注表面症状,不追根因
例如:
- Full GC 多,不一定就是堆小
- CPU 高,不一定就是线程多
8. 动手验证
这一节我给你一个最容易复现、也最有代表性的死锁排查实验,而且我已经在当前环境里实际验证过。
8.1 准备死锁实验代码
新建文件 DeadlockLab.java,内容如下:
java
public class DeadlockLab {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
sleep(200);
synchronized (LOCK_B) {
System.out.println("t1 done");
}
}
}, "deadlock-t1");
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) {
sleep(200);
synchronized (LOCK_A) {
System.out.println("t2 done");
}
}
}, "deadlock-t2");
t1.start();
t2.start();
Thread.sleep(15000);
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}8.2 编译并运行
先编译:
bash
javac DeadlockLab.java再启动它:
bash
java DeadlockLab保持这个进程运行着,然后另开一个终端执行:
bash
jps -l
jstack <pid>8.3 你应该观察到什么
在当前环境里,我实际从 jstack 里看到了这些关键信息:
text
"deadlock-t1"
"deadlock-t2"
Found one Java-level deadlock
Found 1 deadlock.还会看到类似:
text
- waiting to lock ...
- locked ...8.4 这组结果在验证什么
deadlock-t1、deadlock-t2:说明死锁线程被明确识别出来了Found one Java-level deadlock:说明jstack已经直接确认死锁成立waiting to lock/locked:说明工具不仅告诉你“卡住了”,还告诉你“卡在谁身上”
这就是典型的:
- 现象 -> 工具 -> 证据 -> 结论
8.5 你还可以继续做的两个实验
- 写一个无限循环线程,再结合
jstack模拟 CPU 飙高分析 - 写一个小堆 OOM 实验,再配合
-XX:+HeapDumpOnOutOfMemoryError保留 dump
9. 练习建议
下面这些练习做完,这一章会更扎实:
- 人工构造一次 OOM 并完整排查
- 模拟一次 CPU 飙高并定位热点线程
- 记录一次从现象到结论的完整分析过程
- 给自己整理一份“常见故障 -> 第一工具 -> 第二工具 -> 结论模板”的排障表
10. 自测问题
- OOM 排查通常先看哪些信息?
- CPU 飙高为什么往往要结合线程栈分析?
- 为什么 Full GC 频繁不一定只是堆太小?
- 死锁为什么优先用
jstack? - 为什么排障不能一上来就改参数?
11. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
- 排障必须先有现象和证据,再有动作
- OOM、CPU、GC、死锁、类加载冲突各自有不同优先工具
jstack对线程阻塞和死锁分析特别关键jmap、jstat、GC 日志是内存与回收问题的重要证据来源- JVM 排障最终通常要落回代码、对象模型、线程模型和参数配置的综合分析