JVM之常见线上问题排查

一、OOM 排查 完整流程

1. 前置配置(线上必开)

1
2
3
4
5
# 转储堆
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/xxx/heap.hprof
#只要抛出 OOM Error,JVM 立刻退出进程。
-XX:+ExitOnOutOfMemoryError

OOM 自动生成堆快照,避免现场丢失。

OOM后进程状态

OOM只是抛出异常,不是进程自杀。

某条线程分配内存失败,由JVM抛出OOM Error,如果这条线程没有捕获Error,该线程直接终止,JVM本身不会立刻退出!

jvm退出情况

所有非守护线程全部结束。

代码调用System.exit()

JVM 内部致命错误(fatal error,如 hs_err_pid 日志)

操作系统 kill(kill -9)

堆OOM演示:

假设 Tomcat/SpringBoot Web 服务:

  1. 一个 http 请求线程创建大对象,堆满,抛出 OOM;
  2. 该线程未捕获 Error → 这条请求线程死掉
  3. 其他正在运行的线程、线程池里空闲线程还活着

此时服务现状:

  1. 进程 PID 存在,ps 能查到,端口还在监听
  2. 短期现象:
    • 一部分新请求能正常处理(分配内存需求小)
    • 一部分请求一分配内存就再次触发 OOM,线程不断消亡
  3. 连锁恶化(绝大多数线上真实情况) 不断有工作线程因为 OOM 死掉 → 线程池线程耗尽
    → 新请求排队、超时、拒绝
    → 看起来服务 “假死”,端口通但是访问超时
  4. 内存碎片 / GC 压力暴增:频繁 FullGC,CPU 打满,响应极慢

不同OOM类型对进程存活的影响

OOM 类型 进程是否保留 业务可用性
Java heap space(堆 OOM) PID 保留 大概率不可靠,时好时坏
Direct buffer memory(直接内存 OOM) 风险高,容易 JVM 崩溃 极易直接宕机
Metaspace 元空间 OOM 容易引发致命错误 经常直接退出进程
Unable to create new native thread(无法创建线程) 线程不断死亡,最终无可用线程 服务彻底无法接收请求

日志配置及启动脚本

OOM error 是jvm抛出的,jvm会把日志输出到标准错误输出中。

所以需要区分业务日志及标准输出中。

我们配置logback时不要配置业务打印到控制台(标准输出consoleAppender),启动脚本时java -jar app.jar >> app.out 2>&1把标准输出及标准错误输出都写入到app.out里。

日志来源 输出通道 最终位置(你的场景)
log.error (“业务异常”,e) Logback FileAppender 业务 xxx.log(logback 控制)
JVM 自动打印的 OOM 堆栈 进程 stderr app.out(shell 重定向捕获)
System.out.println 进程 stdout app.out
System.err.println 进程 stderr app.out

如果是k8s/systemd 容器环境,容器默认收集 stdout/stderr,只要容器日志驱动正常,kubectl logs 能看到 JVM 打印的 OOM。

2. 排查步骤

配置了前置配置:

先查看标准错误输出日志,有可能直接定位,如果定位不了拿着堆文件进行分析。

未配置前置配置或学习时:

手动导出:

1
jmap -dump:format=b,file=heap.hprof PID

使用 MAT / VisualVM 打开堆文件

核心分析点:

  • 大对象、超大集合
  • 实例数量异常多的类
  • 支配树:查看谁持有引用无法释放
  • 线程栈:定位业务代码位置

3. 常见 OOM 类型

  • Java heap space:堆内存不足、内存泄漏
  • Metaspace:元空间溢出(动态类、CGLIB、反射)
  • Direct Buffer:堆外内存溢出(NIO 未释放)

二、内存泄漏 8 大经典场景

  1. 静态集合类

    static List/Map 全局常驻,对象放入后永不回收。

  2. 单例模式

    单例长期持有业务对象引用。

  3. 资源连接未关闭

    Connection、Statement、ResultSet、IO 流、Socket 只创建不 close。

  4. 长生命周期持有短生命周期对象

    比如缓存、全局上下文持有临时对象。

  5. 内部类 / 匿名内部类

    隐式持有外部类引用,导致外部类无法回收。

  6. 缓存无淘汰策略

    LocalCache、Guava Cache 不设过期、不限制容量。

  7. ThreadLocal 未 remove

    线程复用(线程池)导致弱引用 key 回收,value 强引用泄漏。

  8. 非手动堆外内存

    ByteBuffer 直接内存、Netty 缓冲区未释放。

核心本质:无用对象被强引用持续持有,GC 无法回收


三、死锁排查 流程 + 原理

1. 死锁必要四大条件

  • 互斥条件
  • 请求与保持
  • 不可剥夺
  • 循环等待

2. 排查步骤

  1. 执行
1
jstack PID
  1. 搜索关键字:Found one Java-level deadlock
  2. 直接输出:
  • 互相等待的两个线程
  • 各自持有的锁、等待的锁
  • 精确到类名 + 行号

3. 解决

  • 统一锁顺序
  • 尝试限时锁 tryLock(time)
  • 缩小锁粒度

四、CPU 过高 排查完整步骤(面试高频)

1. 定位高 CPU 线程

  1. 查看进程 CPU
1
top
  1. 查看该进程下所有线程 CPU 占用
1
top -H -p PID
  1. 记录 CPU 最高的线程 ID,转为 16 进制
1
printf "%x\n 线程ID"

2. 定位代码行

1
jstack PID | grep -A 20 十六进制线程ID
  • 直接打印出耗 CPU 方法、堆栈、代码行号

3. 高频导致 CPU 飙高的代码

  • 死循环 while (true) 无休眠
  • 频繁正则、递归过深
  • 大量序列化 / 反序列化
  • 无限自旋 CAS、空轮询
  • 频繁 Full GC(GC 线程占满 CPU)

五、快速总结

  1. OOM

    开启 OOM 自动 dump → jmap 导出 → MAT 分析大对象 / 引用链 → 定位泄漏代码。

  2. 内存泄漏

    静态集合、单例、连接未关闭、ThreadLocal、缓存不淘汰、内部类、堆外内存、长生命周期引用。

  3. 死锁

    jstack 检索 deadlock,查看互相等待锁 + 代码行;解决:统一锁顺序、限时锁。

  4. CPU 过高

    top 定位进程 → top -H 找高 CPU 线程 → 转 16 进制 → jstack 定位具体卡死 / 循环代码。


JVM之常见线上问题排查
http://hanqichuan.com/2026/04/17/java/jvm/JVM之常见线上问题排查/
作者
韩启川
发布于
2026年4月17日
许可协议