线上突然OOM,你的排查顺序是先看日志还是先重启

「Java 进阶之路」系列 Day36

写在前面

前面五篇讲了内存模型、类加载、垃圾回收算法、分代收集、可达性分析这些原理,这一篇是模块五的收官篇,把这些原理落地到一个真实场景:线上服务突然抛出OutOfMemoryError,或者数据库连接池耗尽导致请求大量超时,该怎么一步步定位到根因。这类问题最怕的不是"不会背GC原理",而是"背得出原理,但现场手忙脚乱不知道从哪下手"。


一、是什么:OOM 不是只有一种

OutOfMemoryError报错本身会带一行具体信息,不同的信息对应完全不同的病因,排查方向也完全不一样:

flowchart TB OOM[OutOfMemoryError] --> Heap[Java heap space] OOM --> Meta[Metaspace] OOM --> Overhead[GC overhead<br/>limit exceeded] OOM --> Direct[Direct buffer memory] OOM --> Thread[unable to create<br/>new native thread] Heap --> HeapCause[堆内对象持续增长<br/>通常是内存泄漏<br/>或者堆设置过小] Meta --> MetaCause[加载的类太多常见于动态<br/>生成类的框架或者热部署<br/>没有卸载旧类加载器] Overhead --> OverheadCause[GC频繁回收却几乎<br/>回收不了多少内存<br/>存活对象占满了堆] Direct --> DirectCause[NIO直接内存或者<br/>堆外缓存用多了没释放] Thread --> ThreadCause[线程数超过了操作系统<br/>或者JVM的限制常见于<br/>线程池配置错误或者线程泄漏]

一句话总结区分方式:OutOfMemoryError后面跟的那句话,它已经告诉你是堆、方法区、还是线程数出了问题,不要一律当成"内存不够"就去调大-Xmx了事------盲目调大堆反而可能让本该早点暴露的问题拖得更久,最后在更大的堆上再炸一次,排查更麻烦。


二、为什么必须靠堆转储分析,而不是靠猜

前几篇讲过,垃圾回收器只负责清理"不可达"的对象,如果代码里有一条强引用链一直续着某些本该被丢弃的对象(比如集合类越塞越多却从不清理、监听器注册了从不注销、缓存没有过期策略),这些对象在可达性分析里永远"活着",GC拿它们完全没办法------这正是"内存泄漏"在Java里的真实含义:不是内存真的丢了,而是一批本该死掉的对象,因为一条不该存在的强引用,被迫一直占着位置。

光看监控曲线只能看到"堆内存在持续上涨、Full GC越来越频繁但回收效果越来越差"这个现象,但看不出具体是哪些对象在堆积 。真正定位问题必须靠堆转储文件(Heap Dump)------把某一时刻堆里所有对象的快照导出来,一个个类地统计数量和占用大小:

bash 复制代码
# 手动导出一份堆快照(生产环境更推荐提前配置好自动导出)
jmap -dump:live,format=b,file=heap.hprof <pid>

# 更推荐提前配置:一旦发生OOM,JVM自动导出堆快照再退出,不用等人工手动介入
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/

拿到.hprof文件之后,用Eclipse MAT或者IDEA自带的Profiler打开,重点看两个视图:

  • Histogram(直方图):按类分组,看哪个类的实例数量、占用内存明显不正常(比如某个自定义DTO类实例数量是十万级别,远超合理业务量)
  • Dominator Tree(支配树):找到"保留大小"最大的对象------如果一个对象被回收,能连带释放多少内存,这个指标直接指向真正占大头的那条引用链的根

顺着支配树往下追,通常能直接看到是哪个集合、哪个缓存Map在不断膨胀,再回头去代码里找这个集合到底谁在写入、有没有对应的清理逻辑。


三、怎么用:一次连接池耗尽的排查思路

比内存泄漏更高频的线上问题是数据库连接池耗尽------表现通常是接口大面积超时,日志里能看到类似Timeout waiting for connection from pool这样的报错。排查思路和内存问题类似,都是"先看现象、再找根因、最后验证":

第一步:确认连接到底是"不够用"还是"用不回来"

先看连接池的监控指标(HikariCP、Druid都有暴露):活跃连接数是不是长期顶着最大值不下降。如果活跃连接数一直居高不下,说明连接被借出去之后没有归还,而不是业务量真的需要这么多连接。

第二步:用线程栈定位"卡在哪里不还连接"

bash 复制代码
jstack <pid> > threads.txt

在线程栈里搜索业务线程的堆栈,常见能定位到的几种情况:

  • 某段代码忘记在finally里关闭连接 (或者用了try-with-resources但外层又手动catch吞掉了异常导致连接对象没走到关闭逻辑)------这是最经典的"忘记还连接"
  • 连接被拿到之后卡在一次慢查询上迟迟不返回(比如一条没走索引的SQL全表扫描),连接本身没泄漏,只是每个连接占用时间过长,导致池子里能周转的连接数不够用
  • 事务边界过大:一个事务里夹杂了远程调用(比如调第三方接口、发MQ),远程调用耗时不可控,却一直攥着数据库连接不放,等于把连接的生命周期和一个不可控的外部调用绑在了一起

第三步:区分"连接池太小"还是"代码有泄漏"

如果通过jstack确认线程都在正常处理业务、只是连接周转确实不够快,那是容量规划问题,适当调大连接池最大连接数、拆分大事务、给慢查询加索引就能解决;但如果连接池比正常业务量宽裕很多、活跃连接数却依然长期占满,同时找不到几个线程真的在使用这些连接,那基本可以断定是代码层面的连接泄漏------某处拿到连接后走了一条异常分支,没有执行到归还逻辑,这时候调大连接池只是把问题爆发的时间往后拖,治标不治本。

flowchart TB A[接口大面积超时 连接池报Timeout] --> B[看活跃连接数指标] B --> C[活跃连接数长期顶满] C --> D[jstack看业务线程在干什么] D --> E[线程都在正常处理 只是周转不够快] D --> F[大量线程根本没在用连接 但连接就是还不回去] E --> G[容量规划问题 调大连接池 优化慢查询 拆分大事务] F --> H[代码连接泄漏 回去找没有finally归还的路径]

四、面试追问

Q1:线上出现OutOfMemoryError,第一步应该做什么?

先看OutOfMemoryError后面具体的提示信息(比如Java heap spaceMetaspaceunable to create new native thread),不同提示对应完全不同的病因和排查方向,不能一律当成"堆不够大"就直接调大-Xmx。同时应该提前配置好-XX:+HeapDumpOnOutOfMemoryError,让JVM在发生OOM时自动导出堆快照,方便事后分析,而不是等人工手动介入时现场已经重启、证据已经丢失。

Q2:为什么会出现"对象明明该被回收却没被回收"的内存泄漏?

Java的垃圾回收基于可达性分析,只要有一条从GC Roots出发的强引用链能摸到某个对象,它就永远不会被判定为垃圾。内存泄漏的本质是代码里存在一条不该存在的强引用链,让本该被丢弃的对象持续可达------常见于集合类只增不减、监听器注册了不注销、缓存没有过期淘汰策略。GC本身没有任何问题,问题出在业务代码维护引用关系的方式上。

Q3:拿到堆转储文件之后,具体怎么定位是哪里在泄漏?

用MAT或者Profiler工具打开堆快照,先看Histogram直方图,找到实例数量或占用内存明显异常的类;再用Dominator Tree支配树,找到"保留大小"最大的对象------这个指标反映了如果这个对象被回收能连带释放多少内存,直接指向占用最大的那条引用链的根。顺着这条链往回追到具体的集合或者缓存对象,再回代码里定位是谁在写入、有没有对应的清理逻辑。

Q4:连接池耗尽和内存泄漏在排查思路上有什么共同点?

两者的核心思路是一致的:先通过监控指标确认现象(内存持续增长、还是活跃连接数长期占满),再通过工具还原当时的具体状态(堆转储快照、还是线程栈dump),顺着这份快照找到具体是哪个对象或者哪段代码在制造问题,最后回到源码定位根因。都不能靠猜测直接下结论,必须要有具体的快照数据支撑。

Q5:怎么判断连接池耗尽是容量规划问题还是代码泄漏?

用jstack导出线程栈,观察业务线程实际在做什么。如果大部分线程确实都在正常处理业务、只是连接周转速度跟不上业务量,说明是容量不够,可以调大连接池、优化慢查询、拆分大事务解决;但如果连接池配置已经比正常业务量宽裕很多,活跃连接数却依然长期占满,同时找不到足够多的线程真的在使用这些连接,就说明存在代码层面的连接泄漏------某处拿到连接后走了异常分支没有执行归还逻辑,这种情况调大连接池只是拖延问题爆发的时间。


下一篇预告

模块五(JVM与GC)到这里全部写完。Day37 开始进入模块六------Java 8+ 函数式编程,从Lambda表达式的语法糖本质讲起。

相关推荐
西门老铁1 小时前
UUID 还是雪花 ID?分布式唯一 ID 方案怎么选?
后端
吃饱了得干活1 小时前
Java设计模式实战:一个支付模块的重构之旅,层层递进理解设计模式精髓
后端·设计模式·架构
君顾11 小时前
折扣卡 CPS 小程序定制:从业务流程到系统架构的实践指南
java·开发语言·折扣卡
见不散1 小时前
GPT-5.6 Luna (Batch) 批量处理能力深度评测
java·gpt·batch
江湖十年1 小时前
Go 还是 Golang?可能你一直都搞错了!
后端·面试·go
计算机毕设定制辅导-无忧学长1 小时前
《基于SpringBoot的庭院玫瑰栽培养护知识交互式科普平台设计与实现》
java·spring boot·后端·毕业设计·个性化推荐·协同过滤算法·庭院玫瑰栽培养护知识科普平台
骇客野人1 小时前
SpringBoot数十Jar包批量生产部署落地实施方案(Shell+Systemd完整版)
spring boot·后端·jar
Json____2 小时前
从零构建家政服务平台:一套全栈架构如何打通管理端与移动端-java-springboot
java·spring boot·后端·架构·毕设·wwwoop.com
leeyi2 小时前
命令行里的平台:df_cli 都会什么(第108篇)
后端·aigc·agent