Skip to content

常见问题排查

1. 这是什么

常见问题排查是把前面的 JVM 理论和工具能力真正用起来的阶段。
重点不是再学新概念,而是把问题分析变成可复用的方法。

一句话理解:

  • JVM 学习的终点不是会背术语
  • 而是系统出问题时能快速建立证据链

2. 为什么重要

真正的高级能力,不体现在背了多少概念,而体现在这些时刻:

  • OOM 了,能不能快速知道是堆、元空间还是直接内存问题
  • CPU 飙高了,能不能快速定位是哪个线程、哪段代码
  • Full GC 频繁了,能不能区分是堆太小、对象模型不合理还是内存泄漏
  • 类加载冲突了,能不能判断是依赖版本问题还是类加载器隔离问题

JVM 学习的价值,最终都要落到排障上。

3. 先建立直觉:排障的核心不是“先改参数”,而是先拿证据

排障最容易犯的错就是:

  • 一出问题就重启
  • 一出问题就改参数
  • 一出问题就猜“肯定是 GC”

更稳的思路应该是:

  1. 先看现象
  2. 再收集指标
  3. 再用工具验证
  4. 最后形成结论和动作

可以把它记成:

  • 现象 -> 指标 -> 工具 -> 证据 -> 结论 -> 处理

4. 核心内容

4.1 OOM 排查

先问几个关键问题:

  • 是 Java 堆 OOM 吗?
  • 是元空间 OOM 吗?
  • 是直接内存问题吗?
  • 有没有 Heap Dump?

优先收集:

  • 错误类型
  • JVM 启动参数
  • 堆转储文件
  • jmap -histo
  • GC 日志

常见根因包括:

  • 内存泄漏
  • 缓存过大
  • 单次请求拉起过多对象
  • 长生命周期对象未释放

4.2 Full GC 频繁排查

遇到 Full GC 频繁时,不要第一反应就认为:

  • “堆太小”

还可能是:

  • 老年代存活对象太多
  • 晋升过快
  • 大对象分配异常
  • 内存泄漏
  • 参数配置不合理

优先看:

  • GC 日志
  • 堆对象分布
  • 存活对象规模
  • 是否存在明显热点对象类型

4.3 CPU 飙高排查

CPU 问题最常用的一条路径是:

  1. 先看系统层 CPU 高不高
  2. 找到高 CPU 进程
  3. jstack 看线程栈
  4. 定位热点线程在干什么

常见根因包括:

  • 死循环
  • 自旋重试过多
  • 锁竞争严重
  • 某段业务逻辑复杂度失控

4.4 死锁排查

死锁问题最经典的工具是:

  • jstack

典型现象:

  • 请求卡死
  • 线程数看着还在
  • 但部分核心线程一直不推进

jstack 往往能直接告诉你:

  • 哪些线程互相等待
  • 锁对象是谁
  • 死锁是否成立

4.5 类加载冲突排查

常见症状:

  • ClassNotFoundException
  • NoClassDefFoundError
  • ClassCastException 但看起来“明明是同一个类”
  • 框架启动时报依赖冲突

优先思路:

  • 先看类名和包名
  • 再看依赖版本
  • 再看类加载器
  • 最后看是不是多模块 / 插件环境下的隔离问题

5. 一个通用排障流程

这套流程非常值得反复练:

  1. 明确现象:是慢、卡、OOM、CPU 高,还是 Full GC 频繁
  2. 收集基础信息:时间点、流量、版本、参数、日志
  3. 选择工具:线程看 jstack,堆看 jmap,GC 看 jstat / GC 日志
  4. 建立证据链:不要只看一条信息
  5. 形成结论:明确根因类别
  6. 给出动作:调参数、修代码、改对象模型、调整线程池、优化依赖

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-t1deadlock-t2:说明死锁线程被明确识别出来了
  • Found one Java-level deadlock:说明 jstack 已经直接确认死锁成立
  • waiting to lock / locked:说明工具不仅告诉你“卡住了”,还告诉你“卡在谁身上”

这就是典型的:

  • 现象 -> 工具 -> 证据 -> 结论

8.5 你还可以继续做的两个实验

  1. 写一个无限循环线程,再结合 jstack 模拟 CPU 飙高分析
  2. 写一个小堆 OOM 实验,再配合 -XX:+HeapDumpOnOutOfMemoryError 保留 dump

9. 练习建议

下面这些练习做完,这一章会更扎实:

  • 人工构造一次 OOM 并完整排查
  • 模拟一次 CPU 飙高并定位热点线程
  • 记录一次从现象到结论的完整分析过程
  • 给自己整理一份“常见故障 -> 第一工具 -> 第二工具 -> 结论模板”的排障表

10. 自测问题

  • OOM 排查通常先看哪些信息?
  • CPU 飙高为什么往往要结合线程栈分析?
  • 为什么 Full GC 频繁不一定只是堆太小?
  • 死锁为什么优先用 jstack
  • 为什么排障不能一上来就改参数?

11. 自测核对要点

如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:

  • 排障必须先有现象和证据,再有动作
  • OOM、CPU、GC、死锁、类加载冲突各自有不同优先工具
  • jstack 对线程阻塞和死锁分析特别关键
  • jmapjstat、GC 日志是内存与回收问题的重要证据来源
  • JVM 排障最终通常要落回代码、对象模型、线程模型和参数配置的综合分析