---
title: JVM OOM 完整分类与排查
type: concept
domain: java-concurrency
tags: [jvm, oom, heap, metaspace, directbuffer, gc-overhead, heap-dump, mat]
status: learning
created: 2026-09-22
last_reviewed: 2026-09-22
method: feynman
related_questions:
  - "在什么情况下会发生OOM?都有哪些类型的OOM?都如何解决和监控?"
related_knowledge:
  - ./jvm-gc-algorithms.md
  - ./jvm-gc-select-tune.md
anki_cards: 5
interview_rounds:
  - "2026-09-22-round-1-Q9"
---

# JVM OOM 完整分类与排查

> OOM = JVM 认为无法通过 GC 解决内存不足。完整 8 类，排查三件套：**堆 dump + GC 日志 + 原生内存追踪**。

## 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** 通常是**堆溢出的前兆**

## 关键结论

- **结论 A**：OOM 是 Error 而非 Exception，通常 JVM 不会立即退出（除特定类型或 GC 前最后一次尝试失败）
- **结论 B**：**GC 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 类

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

#### 救命参数组合（生产必备）

```bash
# 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 <pid> 1000`：GC 频率与堆使用率
- `jcmd <pid> GC.heap_info`：堆信息
- `jcmd <pid> VM.native_memory summary`：NMT 原生内存
- `jmap -histo:live <pid>`：对象直方图
- `jstack <pid>`：线程 dump
- `jcmd <pid> 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

1. **先看 OOM 类型**（异常消息，锁定问题区域）
2. **打开 dump**，MAT 看 Dominator Tree 找最大对象持有者
3. **Shortest Path to GC Roots**：判断是不是被"不该引用的强引用"持有
4. **Metaspace 溢出**：`GC.class_histogram` 看 ClassLoader 数量
5. **DirectBuffer 溢出**：`VM.native_memory` 或 JFR 事件分析
6. **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

## 常见误区

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

## 延伸追问

1. **Java heap space 和 GC overhead 有什么关系？**
   - GC overhead 通常是堆溢出的前兆——GC 努力回收但效果差；先出现 GC overhead，随后堆溢出
2. **如何判断是内存泄漏还是正常的堆使用？**
   - 多次 FullGC 后堆使用率仍不下降就是泄漏；MAT Dominator Tree 找"存活最多"的根对象；Shortest Path to GC Roots 看是否被"不该引用的强引用"持有
3. **Metaspace 溢出如何定位 ClassLoader 泄漏？**
   - `jcmd <pid> GC.class_histogram` 看 ClassLoader 数量；查热部署、脚本引擎、反射动态代理；用 `VM.classloader_stats` 事件
4. **DirectByteBuffer 泄漏怎么查？**
   - JDK 9+ 用 Cleaner 自动回收；JDK 8 靠 PhantomReference；`jcmd VM.native_memory`、JFR `jdk.DirectByteBuffer`；Netty 用 `ByteBuf.refCnt()`
5. **生产 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)
```

## 关联题目（题库）

- ⚠️ 《在什么情况下会发生OOM?都有哪些类型的OOM?都如何解决和监控?》— Round 1 Q9, ⭐⭐⭐（漏 DirectBuffer 和 GC overhead）

## 关联知识

- [JVM GC 算法与 STW](./jvm-gc-algorithms.md)
- [JVM GC 器选型与调优](./jvm-gc-select-tune.md)
- [JVM 内存模型](./jvm-memory-model.md)
- [主题地图](./_moc.md)

## Anki 候选卡片

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

---

*最后更新：2026-09-22*
