JVM OOM 完整分类与排查

java-concurrency 📚 learning jvm-oom · jvm · oom · heap · metaspace · directbuffer · gc-overhead · heap-dump · mat

TL;DR(30 秒扫完)

  • OOM 完整 8 类:Java heap space / GC overhead / Metaspace / Direct buffer / thread / array size / compressed class / continuation
  • 救命参数组合:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=... -XX:+ExitOnOutOfMemoryError
  • 排查三件套:堆 dump(MAT)+ GC 日志(-Xlog:gc*)+ 原生内存(jcmd VM.native_memory)
  • DirectBuffer 泄漏:Netty ByteBuf 未 release,JDK 9+ 用 Cleaner
  • GC overhead 通常是堆溢出的前兆

关键结论

结论 AOOM 是 Error 而非 Exception,通常 JVM 不会立即退出(除特定类型或 GC 前最后一次尝试失败)
结论 BGC overhead limit exceeded 是堆溢出的前兆——GC 努力回收但效果差
结论 C救命参数 -XX:+HeapDumpOnOutOfMemoryError 是生产上最重要的排查手段
结论 D不同 OOM 类型需不同排查路径(Metaspace 查 ClassLoader、DirectBuffer 查 Native Memory)
结论 E-XX:+ExitOnOutOfMemoryError 让容器快速重启,配合 dump 保留现场

完整讲解(费曼四步)

STEP 1 · 概念

OOM(OutOfMemoryError)是 JVM 认为无法通过 GC 解决内存不足时抛出的 Error(非 Exception)。完整 8 类,每类触发场景和排查手段不同。

STEP 2 · 大白话

类比:OOM 像"仓库爆仓"——不同爆仓类型对应不同原因:
- 堆溢出:货物堆积太多(缓存无界、监听器未反注册)
- Metaspace:货品种类太多(动态代理、脚本引擎反复编译)
- DirectBuffer:装卸货区的临时堆(Netty ByteBuf 未释放)
- 线程过多:搬运工太多,都占着空间
关键是先看清是哪个区爆仓(看错误消息),才能对症下药。

STEP 3 · 底层

OOM 完整 8 类

#类型触发场景排查手段
1java.lang.OutOfMemoryError: Java heap space缓存无界、集合未清、监听器未反注册、ThreadLocal 未 remove堆 dump + MAT
2GC overhead limit exceededGC >98% 但回收 <2%,堆溢出前兆堆 dump + GC 日志
3Metaspace动态代理(CGLIB/ByteBuddy)、脚本反复编译、热部署反复创建 ClassLoaderjcmd GC.class_histogram
4Direct buffer memoryNetty ByteBuf 未 release、FileChannel.map 未 unmapjcmd VM.native_memory、JFR jdk.DirectByteBuffer
5unable to create new native threadThread 未复用、线程池无上限、ThreadLocal 泄漏导致线程膨胀线程 dump(jstack)
6Requested array size exceeds VM limit一次申请超大数组检查代码数组大小
7Compressed class space-XX:CompressedClassSpaceSize 满调大压缩类空间
8unable to make continuationJava 21+ Loom 协程溢出检查虚拟线程配置

典型触发场景详解

堆溢出:
  • 静态集合持续增长(static Map / static List 无清理)
  • 缓存无 TTL、无淘汰策略
  • 大对象一次性加载(readAllBytes、readLines)
  • 监听器未反注册导致回调链泄漏
  • ThreadLocal 未 remove 导致线程膨胀
Metaspace 溢出:
  • CGLIB/ByteBuddy 动态生成类
  • Groovy/JavaScript 脚本引擎反复编译
  • 热部署反复创建 ClassLoader
  • 反射生成大量代理类
DirectBuffer 溢出:
  • Netty ByteBuf 未 release()
  • FileChannel.map() 未 unmap
  • NIO ByteBuffer.allocateDirect 未 GC 回收(JDK 8 需 PhantomReference 手动清理,JDK 9+ 用 Cleaner)

救命参数组合(生产必备)

# OOM 时自动 dump 堆(救命!)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/dumps/

# OOM 后退出,方便容器重启
-XX:+ExitOnOutOfMemoryError

# GC 日志(JDK 9+)
-Xlog:gc*

# 原生内存追踪(NMT)
-XX:NativeMemoryTracking=detail

# 堆限制
-XX:MaxDirectMemorySize=512m
-XX:MaxMetaspaceSize=512m

排查工具

运行时命令:
  • jstat -gcutil 1000:GC 频率与堆使用率
  • jcmd GC.heap_info:堆信息
  • jcmd VM.native_memory summary:NMT 原生内存
  • jmap -histo:live :对象直方图
  • jstack :线程 dump
  • jcmd GC.class_histogram:类直方图(查 ClassLoader 泄漏)
可视化分析:
  • MAT(Eclipse Memory Analyzer):Dominator Tree、Leak Suspects、Shortest Path to GC Roots
  • VisualVM
  • JProfiler
  • JFR(Java Flight Recorder):jdk.GCPhasePause、jdk.GCHeapSummary、jdk.DirectByteBuffer
  • Arthas dashboard
  • Prometheus + JVMExporter + Grafana

排查 SOP

  • 先看 OOM 类型(异常消息,锁定问题区域)
  • 打开 dump,MAT 看 Dominator Tree 找最大对象持有者
  • Shortest Path to GC Roots:判断是不是被"不该引用的强引用"持有
  • Metaspace 溢出:GC.class_histogram 看 ClassLoader 数量
  • DirectBuffer 溢出:VM.native_memory 或 JFR 事件分析
  • GC overhead:通常是堆溢出的前兆,先看堆 dump

解决矩阵

问题短期处置长期解决
堆溢出kill + 换实例、扩堆修内存泄漏、优化分配
Metaspace 溢出调大 -XX:MaxMetaspaceSize排查 ClassLoader 泄漏
DirectBuffer 溢出kill + 重启显式释放、JDK 9+ Cleaner
GC overhead扩堆、换 GC 器修泄漏、优化分配
线程过多kill 线程、扩容线程池化、ThreadLocal 管理

STEP 4 · 简化

一句话总结:OOM 完整 8 类,先按错误消息锁定类型,再对症下药。救命参数+排查三件套是生产必备。
记忆口诀:
  • 堆 / Metaspace / Direct / 线程 / 数组 / 类空间 / 协程 = 7 类主战场
  • GC overhead = 堆溢出前兆
  • 救命参数:HeapDumpOnOutOfMemoryError + ExitOnOutOfMemoryError + Xlog:gc*
  • 排查三件套:dump + 日志 + NMT

常见误区

"OOM 只有堆溢出"
完整 8 类,Metaspace / DirectBuffer / 线程 / 数组 / 协程 都要考虑
"GC overhead 是独立问题"
通常是堆溢出的前兆,先看堆 dump
"OOM 直接重启就好"
重启前必须先 dump,否则丢失现场,无法定位
"Netty ByteBuf 会自动释放"
必须显式 release(),JDK 9+ 有 Cleaner 兜底但会延迟
"-XX:MaxDirectMemorySize 是堆内存限制"
它是堆外直接内存限制,与堆独立

延伸追问

Java heap space 和 GC overhead 有什么关系?
GC overhead 通常是堆溢出的前兆——GC 努力回收但效果差;先出现 GC overhead,随后堆溢出
如何判断是内存泄漏还是正常的堆使用?
多次 FullGC 后堆使用率仍不下降就是泄漏;MAT Dominator Tree 找"存活最多"的根对象;Shortest Path to GC Roots 看是否被"不该引用的强引用"持有
Metaspace 溢出如何定位 ClassLoader 泄漏?
jcmd GC.class_histogram 看 ClassLoader 数量;查热部署、脚本引擎、反射动态代理;用 VM.classloader_stats 事件
DirectByteBuffer 泄漏怎么查?
JDK 9+ 用 Cleaner 自动回收;JDK 8 靠 PhantomReference;jcmd VM.native_memory、JFR jdk.DirectByteBuffer;Netty 用 ByteBuf.refCnt()
生产 OOM 的应急 SOP?
kill 前 -XX:+HeapDumpOnOutOfMemoryError 保证 dump;kill -15 而非 -9;保留 dump 做 MAT 分析;扩堆作为临时措施;根因排查优先修泄漏

速查表

OOM 8 类:
  1. Java heap space (最常见)
  2. GC overhead (堆溢出前兆)
  3. Metaspace (ClassLoader 泄漏)
  4. Direct buffer memory (Netty 未 release)
  5. unable to create new native thread
  6. Requested array size exceeds VM limit
  7. Compressed class space
  8. unable to make continuation (Loom)

救命参数:
  -XX:+HeapDumpOnOutOfMemoryError
  -XX:HeapDumpPath=/path
  -XX:+ExitOnOutOfMemoryError
  -Xlog:gc*
  -XX:NativeMemoryTracking=detail

排查三件套:
  1. 堆 dump + MAT (Dominator Tree / Shortest Path to GC Roots)
  2. GC 日志 -Xlog:gc*
  3. 原生内存 jcmd VM.native_memory (NMT)

Anki 候选卡片

Q: OOM 完整 8 类?
A: Java heap space / GC overhead / Metaspace / Direct buffer / native thread / array size / compressed class / continuation
Q: GC overhead limit exceeded 是什么?
A: GC >98% 但回收 <2%,通常是堆溢出的前兆
Q: OOM 救命参数?
A: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=... -XX:+ExitOnOutOfMemoryError
Q: Metaspace 溢出怎么定位 ClassLoader 泄漏?
A: jcmd GC.class_histogram、VM.classloader_stats 事件
Q: Netty ByteBuf 内存泄漏怎么防?
A: 显式 release(),JDK 9+ Cleaner 兜底;jcmd VM.native_memory、JFR jdk.DirectByteBuffer

关联题目

  • ⚠️ 《在什么情况下会发生OOM?都有哪些类型的OOM?都如何解决和监控?》— Round 1 Q9, ⭐⭐⭐(漏 DirectBuffer 和 GC overhead)
  • 关联知识

    OOM 完整 8 类,每类触发场景和排查手段不同;救命参数 HeapDump+Exit 是生产最重要排查手段
    OOM 像仓库爆仓:堆=货物堆积、Metaspace=货品种类、DirectBuffer=临时堆、线程=搬运工
    ✦ 记 忆 口 诀 ✦
    救命三参数 HeapDump+HeapDumpPath+Exit;排查三件套 堆 dump+GC 日志+原生内存
    关键可视化
    OOM 完整 8 类
    flowchart TB
      O["OOM 8 类"] --> O1["1. Java heap space<br/>缓存无界、监听器未反注册、ThreadLocal 未 remove"]
      O --> O2["2. GC overhead limit exceeded<br/>GC>98% 但回收<2%<br/>堆溢出前兆"]
      O --> O3["3. Metaspace<br/>动态代理/脚本反复编译/热部署"]
      O --> O4["4. Direct buffer memory<br/>Netty ByteBuf 未 release<br/>FileChannel.map 未 unmap"]
      O --> O5["5. unable to create new native thread<br/>Thread 未复用/线程池无上限"]
      O --> O6["6. Requested array size exceeds VM limit<br/>一次申请超大数组"]
      O --> O7["7. Compressed class space<br/>压缩类空间满"]
      O --> O8["8. unable to make continuation<br/>Java 21+ Loom 协程溢出"]
    救命参数组合
    flowchart LR
      P["生产救命三参数"] --> P1["-XX:+HeapDumpOnOutOfMemoryError<br/>OOM 时自动 dump"]
      P --> P2["-XX:HeapDumpPath=/var/log<br/>dump 路径"]
      P --> P3["-XX:+ExitOnOutOfMemoryError<br/>OOM 后退出容器快速重启"]
      P1 -.-> D["MAT 分析 dump<br/>看支配树和泄漏点"]
      P3 -.-> R["配合 dump 保留现场"]
    排查三件套
    flowchart TB
      D["OOM 排查"] --> T1["堆 dump(MAT)<br/>看支配树、泄漏点<br/>-XX:+HeapDumpOnOutOfMemoryError"]
      D --> T2["GC 日志(-Xlog:gc*)<br/>看 GC 频率、STW、FullGC<br/>-Xlog:gc*:file=gc.log"]
      D --> T3["原生内存(jcmd VM.native_memory)<br/>看堆外内存占用<br/>jcmd VM.native_memory summary"]
    知识关系

    ⬆️ 前置(Prerequisite)

    jvm-gc-algorithmsjvm-gc-select-tune

    🔄 延伸(Extends)

    暂无

    ⚡ 对比(Contrast)

    GC 选型— GC 选型讲'选什么',OOM 排查讲'出问题怎么办'——选型错误会导致 OOM
    🎯 概念 📏 规则 ⚠️ 误区 🔍 追问 ✨ 口诀 共 0 张卡,点击翻面