OOM 引起原因以及如何排查?
结论先行:OOM 不是一种错误,而是一族错误。先看异常消息的后缀定位区域(Java heap space / Metaspace / Direct buffer memory / unable to create new native thread),再看是泄漏还是容量不足:Full GC 后占用不降是泄漏,降了就说明分配速率或上限有问题。排查的黄金三件套是 GC 日志、堆转储(heap dump)、分配热点,缺一个都只能靠猜。
一、按消息定位区域
| OOM 消息 | 区域 | 常见原因 |
|---|---|---|
Java heap space | 堆 | 泄漏、一次加载超大文件、缓存无上限 |
GC overhead limit exceeded | 堆 | 98% 时间在 GC 却回收不到 2%,堆几乎被存活对象占满 |
Metaspace | 元空间 | 动态生成类、热部署、CGLIB 代理膨胀 |
Compressed class space | 元空间(压缩类指针区) | 类数量过多,默认上限 1G |
Direct buffer memory | 堆外 | Netty 未释放、allocateDirect 无节制 |
unable to create new native thread | 线程栈 | 线程数触顶、ulimit -u 限制、每请求一线程 |
Requested array size exceeds VM limit | 堆 | 单次申请接近 Integer.MAX_VALUE |
Out of memory: Kill process | 操作系统 | 容器 cgroup 限制,JVM 被内核杀掉(137) |
二、排查流程
告警:OOM / 频繁 Full GC
│
├─ 1. 进程还在吗?
│ ├─ 在 → jmap/jcmd 导出堆,jstat 看趋势
│ └─ 不在(137)→ 查 dmesg 与容器事件,确认是否 cgroup OOMKilled
│
├─ 2. 异常消息指向哪个区域?
│ ├─ 堆 → 看 dump,找最大保留集对象
│ ├─ 元空间 → jcmd GC.class_stats / 看动态类数量
│ ├─ 直接内存 → NMT(Native Memory Tracking)
│ └─ 线程 → jstack 数线程,按栈顶归类
│
├─ 3. 是泄漏还是不够用?
│ └─ Full GC 后老年代是否回落 → 不降=泄漏,降了=分配过快/上限小
│
└─ 4. 定位到代码 → 复现 → 修 → 加监控
三、现场保留与取证
# 启动即配置,事故时才有现场
java -Xms4g -Xmx4g \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/oom.hprof \
-XX:+ExitOnOutOfMemoryError \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filesize=50m,filecount=5 \
-jar app.jar
# 存活进程主动取证
jcmd 1234 GC.heap_dump /tmp/live.hprof
jmap -histo:live 1234 | head -30 # 对象数量与占用 Top30
jcmd 1234 Thread.print > /tmp/threads.txt # 线程栈,看线程数是否异常
jcmd 1234 VM.native_memory summary # 需先加 -XX:NativeMemoryTracking=summary
// 最小复现:一个只进不出的本地缓存
public class OomRepro {
private static final java.util.Map<String, byte[]> CACHE = new java.util.HashMap<>();
public static void main(String[] args) {
for (int i = 0; ; i++) {
CACHE.put("k" + i, new byte[1024 * 1024]); // 每秒增长 1MB,永不淘汰
}
}
}
// 运行:java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./oom.hprof OomRepro
四、用 MAT 找「谁在占内存」
打开 dump 后按这个顺序看:
| 视图 | 看什么 | 结论形态 |
|---|---|---|
| Histogram | 按类统计实例数与浅堆 | 哪个类实例异常多 |
| Dominator Tree | 按保留堆(retained heap)排序 | 哪个对象真正压住了内存 |
| Path to GC Roots | 排除弱引用/软引用后的引用链 | 谁持有它、为什么回收不掉 |
| Leak Suspects | 自动生成的疑似泄漏报告 | 快速定位大块占用 |
五、常见追问
| 追问 | 回答要点 |
|---|---|
| dump 文件太大导不出来怎么办 | 先 jmap -histo:live 看趋势,必要时对单机限流后导;也可用 jcmd GC.heap_dump 的 -all 参数排除不可达对象 |
| 生产不能重启怎么取证 | Arthas heapdump、jmap 在线导出;注意 dump 会触发 Full GC 造成秒级停顿 |
| 元空间 OOM 怎么查 | -Xlog:class+load 看类增长、jcmd GC.class_stats 看类加载器分布,重点查热部署与动态代理 |
| 堆外泄漏怎么定位 | NMT 的 Internal / Other 项持续增长;或 -XX:MaxDirectMemorySize 卡死后看 DirectByteBuffer 的 Cleaner |
为什么加大 -Xmx 有时反而更糟 | 停顿变长、单次 GC 更贵,且泄漏仍在,只是推迟了爆发时间 |
小结
OOM 排查是一条固定流水线:看消息定区域 → 看回收率定性质 → dump 找持有者 → 改代码或改容量。真正决定效率的不是工具,而是有没有在启动参数里留下现场。