Java 内存溢出(OOM)排查实战指南:从复现到 MAT Dump 分析
内存溢出(OutOfMemoryError,简称 OOM)是 Java 开发中最常见的线上问题之一,几乎所有 Java 工程师都会遇到。它通常发生在堆(Heap)、方法区(Metaspace)、直接内存(Direct Memory)等区域内存不足时。
本文从复现 OOM、监控与捕获、Heap Dump、MAT 分析四个实战维度,带你一步步排查 OOM。强烈建议结合实际代码运行练习(用 IDEA 或命令行)。
一、OOM 常见类型一览表(先了解敌人)
| OOM 类型 | 错误信息示例 | 常见原因 | 发生区域 |
|---|---|---|---|
| Java heap space | java.lang.OutOfMemoryError: Java heap space | 对象过多、内存泄漏、大数组 | 堆(Heap) |
| Metaspace | java.lang.OutOfMemoryError: Metaspace | 类加载过多、动态代理、反射滥用 | 方法区(Metaspace) |
| Unable to create new native thread | java.lang.OutOfMemoryError: unable to create new native thread | 线程过多(线程池无上限) | 系统内存(进程级) |
| Direct buffer memory | java.lang.OutOfMemoryError: Direct buffer memory | 堆外内存滥用(Netty ByteBuf 未 release) | 直接内存(Off-Heap) |
| GC overhead limit exceeded | java.lang.OutOfMemoryError: GC overhead limit exceeded | GC 频繁但回收很少(内存泄漏) | 堆 |
结论:堆 OOM 最常见(占 80%+),其次是 Metaspace 和直接内存。
二、复现 OOM(实战第一步:本地模拟线上问题)
用简单代码复现各种 OOM,方便理解和测试。运行时加 JVM 参数 -Xmx128m(限制堆最大 128MB)来加速复现。
1. 堆 OOM(Java heap space)
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次加 1MB 数组
}
}
}
- 运行:
java -Xmx128m HeapOOM - 预期:很快 OOM,日志显示 “Java heap space”
2. Metaspace OOM
用 CGLIB 生成海量类(模拟 Spring AOP 滥用)。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class MetaspaceOOM {
public static void main(String[] args) {
int i = 0;
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create();
System.out.println(++i);
}
}
}
- 运行:
java -XX:MaxMetaspaceSize=128m MetaspaceOOM - 预期:类加载过多,导致 Metaspace OOM
3. 直接内存 OOM(Direct buffer memory)
import java.nio.ByteBuffer;
public class DirectOOM {
public static void main(String[] args) {
while (true) {
ByteBuffer.allocateDirect(1024 * 1024 * 10); // 每次 10MB 直接内存
}
}
}
- 运行:
java -Xmx128m -XX:MaxDirectMemorySize=128m DirectOOM - 预期:直接内存耗尽 OOM
4. 线程 OOM(unable to create new native thread)
public class ThreadOOM {
public static void main(String[] args) {
for (int i = 0; ; i++) {
new Thread(() -> {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("线程数:" + i);
}
}
}
- 运行:直接运行,视系统线程上限而定(Linux 默认 ~8000+)
- 预期:线程过多 OOM
复现小贴士:用小内存参数(如 -Xmx128m)加速;线上别这么玩。
三、监控与捕获 OOM(实战第二步:线上准备)
1. JVM 参数配置(必须加这些)
-XX:+HeapDumpOnOutOfMemoryError # OOM 时自动 Dump 堆
-XX:HeapDumpPath=/data/dump.hprof # Dump 文件路径
-XX:+PrintGCDetails # 打印 GC 细节
-XX:+PrintGCTimeStamps # GC 时间戳
-Xloggc:/data/gc.log # GC 日志
-XX:MaxMetaspaceSize=256m # 限制 Metaspace(默认无限)
-XX:MaxDirectMemorySize=128m # 限制直接内存
2. 监控工具(线上必备)
- jstat:实时监控 GC、内存使用
jstat -gcutil pid 1000(每秒输出) - jmap:查看堆使用
jmap -heap pid - VisualVM / JConsole:图形化监控(本地/远程连接)
- Arthas:线上神器
java -jar arthas-boot.jar→heapdump /data/dump.hprof - Prometheus + Grafana:线上监控堆使用率、GC 频率
捕获技巧:OOM 前看 GC 日志,如果 Old 区持续上涨且 Full GC 回收很少 → 99% 是内存泄漏。
四、Heap Dump 与分析准备(实战第三步:获取证据)
1. 如何 Dump Heap
- 自动 Dump:用上面 JVM 参数,OOM 时自动生成
- 手动 Dump:
jmap -dump:live,format=b,file=/data/dump.hprof pid(推荐 live,只 Dump 存活对象)jcmd pid GC.heap_dump /data/dump.hprof(JDK 9+ 推荐)- Arthas:
heapdump /data/dump.hprof --live
注意:Dump 时会暂停 JVM(STW),线上高峰期慎用;Dump 文件很大(堆大小的 1~2 倍),准备足够磁盘。
2. 下载与传输
线上 Dump 后,用 scp / rsync 下载到本地:scp user@host:/data/dump.hprof .
五、MAT Dump 分析(实战第四步:找出元凶)
MAT(Eclipse Memory Analyzer Tool)是最强 Heap Dump 分析工具,下载地址:https://eclipse.dev/mat/(免费)。
1. 打开 Dump 文件
- 启动 MAT → File → Open Heap Dump → 选择 hprof 文件
- 等待解析(大文件需几分钟)
2. 核心视图与分析步骤(最实用流程)
- Overview:看总体情况(堆大小、类数、对象数、线程数)
- Leak Suspects(最重要!):MAT 自动检测可疑泄漏点
- Problem Suspect 1:通常是最大的对象或集合
- 看 Description:如 “One instance of java.util.HashMap loaded by … occupies 80MB”
- Dominator Tree(支配树):看保留集最大的对象(前 10 名通常是元凶)
- 展开树:看哪个 Map/List/Set 占内存最大
- 右键 → Path To GC Roots → Exclude weak/soft references:找谁持有它不放
- Histogram:按类统计对象数/大小
- 过滤 “java.util.*”:看哪些集合类对象最多
- 右键 → Merge Shortest Paths to GC Roots
- Threads:看线程栈,检查 ThreadLocal 或线程持有的对象
3. 经典 OOM 分析案例
- 案例1:HashMap 泄漏(最常见)
- MAT 中 Dominator Tree 前排是 HashMap
- 找 GC Roots:通常是静态 Map 或 ThreadLocalMap
- 修复:加过期策略 / remove ThreadLocal
- 案例2:大数组(如缓存图片)
- Histogram 中 byte[] / char[] 对象很大
- 找持有者:可能是 List 或缓存类
- 案例3:Metaspace OOM
- 用 jcmd / jmap 看类加载数
- MAT 看 ClassLoader 树
MAT 小技巧:
- OQL 查询:
SELECT * FROM java.util.HashMap WHERE size > 1000(SQL-like 查大集合) - Compare Baskets:对比两个 Dump(前 vs 后)看增长对象
- 内存大时用 64 位 MAT + 加堆
-Xmx8g
六、总结 & 预防 OOM 的最佳实践
排查口诀:
- 先看日志:哪个区域 OOM
- 复现本地:模拟代码
- Dump Heap:自动/手动
- MAT 分析:Leak Suspects → Dominator Tree → GC Roots
预防:
- 设置合理堆大小(-Xmx 至少机器内存 1/2 ~ 3/4)
- 用 Caffeine / Guava Cache 代替裸 HashMap
- ThreadLocal 用完 remove()
- 监控 GC / 内存(Prometheus)
- 线上 Dump 前准备好路径和空间
掌握这个流程,90% 的 OOM 都能快速定位。
有具体案例想模拟(比如 Netty OOM)、MAT 安装问题、或某个步骤代码细节?欢迎继续问~