JVM之常见线上问题排查
一、OOM 排查 完整流程
1. 前置配置(线上必开)
1 | |
OOM 自动生成堆快照,避免现场丢失。
OOM后进程状态
OOM只是抛出异常,不是进程自杀。
某条线程分配内存失败,由JVM抛出OOM Error,如果这条线程没有捕获Error,该线程直接终止,JVM本身不会立刻退出!
jvm退出情况
所有非守护线程全部结束。
代码调用System.exit()。
JVM 内部致命错误(fatal error,如 hs_err_pid 日志)
操作系统 kill(kill -9)
堆OOM演示:
假设 Tomcat/SpringBoot Web 服务:
- 一个 http 请求线程创建大对象,堆满,抛出 OOM;
- 该线程未捕获 Error → 这条请求线程死掉;
- 其他正在运行的线程、线程池里空闲线程还活着;
此时服务现状:
- 进程 PID 存在,ps 能查到,端口还在监听
- 短期现象:
- 一部分新请求能正常处理(分配内存需求小)
- 一部分请求一分配内存就再次触发 OOM,线程不断消亡
- 连锁恶化(绝大多数线上真实情况) 不断有工作线程因为 OOM 死掉 → 线程池线程耗尽
→ 新请求排队、超时、拒绝
→ 看起来服务 “假死”,端口通但是访问超时 - 内存碎片 / 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 | |
使用 MAT / VisualVM 打开堆文件
核心分析点:
- 大对象、超大集合
- 实例数量异常多的类
- 支配树:查看谁持有引用无法释放
- 线程栈:定位业务代码位置
3. 常见 OOM 类型
- Java heap space:堆内存不足、内存泄漏
- Metaspace:元空间溢出(动态类、CGLIB、反射)
- Direct Buffer:堆外内存溢出(NIO 未释放)
二、内存泄漏 8 大经典场景
静态集合类
static List/Map全局常驻,对象放入后永不回收。单例模式
单例长期持有业务对象引用。
资源连接未关闭
Connection、Statement、ResultSet、IO 流、Socket 只创建不 close。
长生命周期持有短生命周期对象
比如缓存、全局上下文持有临时对象。
内部类 / 匿名内部类
隐式持有外部类引用,导致外部类无法回收。
缓存无淘汰策略
LocalCache、Guava Cache 不设过期、不限制容量。
ThreadLocal 未 remove
线程复用(线程池)导致弱引用 key 回收,value 强引用泄漏。
非手动堆外内存
ByteBuffer 直接内存、Netty 缓冲区未释放。
核心本质:无用对象被强引用持续持有,GC 无法回收
三、死锁排查 流程 + 原理
1. 死锁必要四大条件
- 互斥条件
- 请求与保持
- 不可剥夺
- 循环等待
2. 排查步骤
- 执行
1 | |
- 搜索关键字:
Found one Java-level deadlock - 直接输出:
- 互相等待的两个线程
- 各自持有的锁、等待的锁
- 精确到类名 + 行号
3. 解决
- 统一锁顺序
- 尝试限时锁
tryLock(time) - 缩小锁粒度
四、CPU 过高 排查完整步骤(面试高频)
1. 定位高 CPU 线程
- 查看进程 CPU
1 | |
- 查看该进程下所有线程 CPU 占用
1 | |
- 记录 CPU 最高的线程 ID,转为 16 进制
1 | |
2. 定位代码行
1 | |
- 直接打印出耗 CPU 方法、堆栈、代码行号
3. 高频导致 CPU 飙高的代码
- 死循环 while (true) 无休眠
- 频繁正则、递归过深
- 大量序列化 / 反序列化
- 无限自旋 CAS、空轮询
- 频繁 Full GC(GC 线程占满 CPU)
五、快速总结
OOM
开启 OOM 自动 dump → jmap 导出 → MAT 分析大对象 / 引用链 → 定位泄漏代码。
内存泄漏
静态集合、单例、连接未关闭、ThreadLocal、缓存不淘汰、内部类、堆外内存、长生命周期引用。
死锁
jstack 检索 deadlock,查看互相等待锁 + 代码行;解决:统一锁顺序、限时锁。
CPU 过高
top 定位进程 → top -H 找高 CPU 线程 → 转 16 进制 → jstack 定位具体卡死 / 循环代码。