Skip to content
Go back

OOM类型全景排查——从heap到direct memory的四种溢出路径

OOM 类型全景:四种溢出,四种排查路径

一句话结论(30s)

OOM 排查的本质是先按错误信息定位溢出区域,再按区域走对应的排查路径——因为堆、元空间、栈、直接内存这四条路径的根因和工具完全不同。核心方法论是:堆 OOM 用 MAT 支配树找保留内存最大的泄漏源,Metaspace OOM 查 ClassLoader 动态生成类,栈溢出看 jstack 里的递归栈帧,直接内存 OOM 看 NIO 缓冲池。权衡是每种 OOM 都要对症下药,用错工具(比如拿堆的工具查直接内存)只会南辕北辙。

核心原理(2min)

四种溢出路径及对应排查:① Java heap space 由大对象或长生命周期对象持有无用引用造成,用 -XX:+HeapDumpOnOutOfMemoryError 抓 dump,MAT 的 Leak Suspects / 支配树定位保留内存最大的对象及其 GC Root 引用链;② Metaspace 由 CGLIB/Groovy/JSP 无限生成类、ClassLoader 未回收造成,用 jstat -gc 看 M 列趋势、jcmd VM.classloader_stats 看每个 ClassLoader 加载类数;③ StackOverflowError 由无限递归或死循环调用链造成,jstack 里同一方法反复出现几百上千栈帧即递归信号;④ Direct buffer memory 由 NIO allocateDirect 未释放且堆内存充足导致 GC 不触发造成,用 jcmd VM.native_memory summary 和 jconsole 的 BufferPool 观察,修复靠显式 cleaner().clean() 或 -XX:MaxDirectMemorySize 设上限。

底层深入(5-10min)

1. Java heap space(堆 OOM)

根因:对象太多(大对象)或内存泄漏(长生命周期对象持有无用引用)。

排查

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof
→ jmap -dump:live → MAT 打开 → Leak Suspects 或 Dominator Tree
→ 找保留内存最多的对象 → 看它的 GC Root 引用链

MAT 支配树:一个对象”支配”了它不可达后也必然不可达的所有对象。“保留大小”最大的那个就是值得怀疑的泄漏源。

想一想:为什么”支配树的保留大小”能定位泄漏源?因为一个对象支配的对象 = 它死掉后也跟着死掉的对象,保留大小最大的那个,就是”拖着最多内存一起活”的根,顺藤摸瓜看它的 GC Root 引用链即可找到泄漏点。

2. Metaspace(元空间 OOM)

根因:动态载入大量类——CGLIB 无限生成代理类、Groovy 脚本反复编译、JSP 无限加载。ClassLoader 未回收导致其下所有类元数据永久占用。

想一想:为什么 CGLIB/JSP 会指向 Metaspace OOM 而不是堆 OOM?因为每生成一个类都会在元空间占一份类元数据(方法字节码、常量池等),而 ClassLoader 不回收时,它下面的所有类元数据就永远占着 Metaspace——和堆里的对象泄漏是两条完全不同的路径。

排查

jstat -gc <pid>  # 看 M 列 (Metaspace) 增长趋势
jcmd <pid> VM.classloader_stats  # 看哪个 ClassLoader 加载了多少类

3. StackOverflowError(栈溢出)

根因:递归过深(无终止条件或终止条件永远达不到)、无限循环调用链。

排查:jstack 看到同一个方法反复出现几百上千次栈帧——这是明显的递归信号。

4. Direct buffer memory(直接内存 OOM)

根因:NIO ByteBuffer.allocateDirect() 分配堆外内存未释放,且 GC 迟迟不触发(堆内存充足 → 堆外一直不回收)。

想一想:为什么堆外内存泄漏时,GC 不来”救场”?因为直接内存不在堆里,GC 只在堆紧张时才顺带触发 DirectByteBuffer 的 cleaner 回收堆外;堆内存充足时 GC 根本不触发,堆外就一直堆着不释放。

排查

jcmd <pid> VM.native_memory summary  # 看 Native Memory 的 Internal 区域
jconsole MBeans java.nio BufferPool direct MemoryUsed

修复:显式 ((DirectBuffer) buf).cleaner().clean() 手动释放;或 -XX:MaxDirectMemorySize=512m 设置上限防止无限增长。

四种 OOM 速查

错误信息区域常见原因
Java heap space大对象/集合泄漏
Metaspace元空间CGLIB/JSP 无限生成类
StackOverflowError无限递归
Direct buffer memory直接内存NIO 堆外内存未释放

章末提问

追问 1:遇到 Java heap space 你第一步做什么?

回答思路:结论先行——先开 -XX:+HeapDumpOnOutOfMemoryError 抓 dump,再用 MAT 的支配树找”保留大小”最大的对象。因为堆 OOM 的根因要么是大对象、要么是长生命周期对象持有无用引用,支配树能定位”拖住最多内存”的对象及其 GC Root 引用链,比肉眼翻代码快得多。

追问 2:怎么区分是 Metaspace 泄漏还是堆泄漏?

回答思路:结论先行——看错误信息加工具维度:Metaspace 报 Metaspace 错误,用 jstat -gc 看 M 列增长趋势、jcmd VM.classloader_stats 看 ClassLoader 加载类数。因为元空间 OOM 的根因是 CGLIB/Groovy/JSP 无限生成类、ClassLoader 不回收,和堆里对象泄漏是两套工具;拿堆的工具(jmap)查元空间就是南辕北辙。

追问 3:直接内存 OOM 为什么堆内存充足也会发生?怎么修?

回答思路:结论先行——因为堆外内存不被 GC 主动回收,堆充足时 GC 不触发、DirectByteBuffer 的 cleaner 就一直不执行。因为 allocateDirect 分配的内存不在堆里,GC 只看堆压力;修复要么显式 ((DirectBuffer) buf).cleaner().clean() 手动释放,要么 -XX:MaxDirectMemorySize 设上限防止无限增长,排查用 jcmd VM.native_memory summary 和 jconsole 的 BufferPool。


Share this post on:

Previous Post
happens-before规则详解:JMM的8条核心约束
Next Post
JVM运行时数据区——每个字节码在哪里执行?