彻底吃透 Java OOM 异常:从原理、场景、排查到解决方案全攻略
在 Java 后端开发里,OOM(OutOfMemoryError)绝对是线上最让人 “头皮发麻” 的问题之一。它不像普通异常那样好定位,往往服务跑着跑着突然崩掉,日志寥寥几句,让人无从下手。
这篇文章就把 OOM 讲得通透、详细、能直接用于实战,从是什么、为什么、有哪些类型、怎么排查、怎么根治,一次性讲全。
一、什么是 OOM?
OOM 全称OutOfMemoryError,意思是:JVM 无法为新对象分配内存,且垃圾回收也无法腾出足够空间,最终抛出的致命错误。
重点理解三句话:
- OOM 是Error,不是 Exception,一旦出现,应用基本不可用。
- 不是 “内存不够”,而是内存被占满、且回收不了。
- 90% 的 OOM 不是 JVM 内存太小,而是代码问题。
二、OOM 出现的根本原因
一句话总结:程序不断创建对象 → 对象一直被引用 → GC 无法回收 → 内存被撑爆 → 新对象无处安放 → OOM
常见根源:
- 内存泄漏:对象不用了,但一直被持有,GC 无法回收
- 一次性加载过多数据:不分页查全表、大文件全量读入内存
- 资源未关闭:连接、流、线程池滥用
- 静态集合无限添加对象
- JVM 参数不合理:堆、元空间设置过小
- 动态生成大量类:导致元空间溢出
三、JVM 内存区域与 OOM 的关系
Java 虚拟机把内存分成不同区域,每个区域都可能 OOM:
- Heap 堆:存放对象实例 →最常见 OOM
- Metaspace 元空间:存放类信息、方法、常量、代理类
- 虚拟机栈:方法调用栈 → 栈溢出
- 本地方法栈:Native 方法使用
- 直接内存:NIO 使用的堆外内存
- 程序计数器:唯一不会 OOM 的区域
下面我们逐个讲最常出现的 6 种 OOM。
四、6 种最经典 OOM 场景详解
1. 堆内存溢出(Java heap space)
错误信息:
java.lang.OutOfMemoryError: Java heap space原因:堆内存被占满,不断创建对象,且都被强引用,无法 GC。
典型代码:
List<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); }真实业务场景:
- 不分页查询数据库全表数据
- 大文件一次性全部加载到内存
- 死循环创建对象
- 静态集合无限添加数据
这是线上最常见的 OOM。
2. 元空间溢出(Metaspace)
错误信息:
java.lang.OutOfMemoryError: Metaspace原因:加载的类太多,元空间放不下。
常见场景:
- 大量动态代理(CGLIB、MyBatis、Spring 代理)
- 反射频繁生成类
- 热部署过多
- 自定义类加载器未正确释放
JVM 参数:
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m3. 直接内存溢出(Direct buffer memory)
错误信息:
java.lang.OutOfMemoryError: Direct buffer memory原因:NIO 使用ByteBuffer.allocateDirect()申请的堆外内存耗尽。
常见场景:
- Netty、MINA 等 NIO 框架
- 大文件上传下载、流媒体处理
4. 栈溢出(StackOverflowError)
错误信息:
java.lang.StackOverflowError原因:方法调用层级太深,栈帧数量超过栈最大深度。
典型代码:
public void loop() { loop(); // 无限递归 }业务场景:
- 递归没有出口
- 循环调用 A→B→A→B
5. 无法创建本地线程
错误信息:
java.lang.OutOfMemoryError: unable to create new native thread原因:每个线程都占用栈内存,线程数超出系统限制。
场景:
- 代码里随便
new Thread() - 线程池参数不合理,maximumPoolSize 过大
- 高并发下无节制创建线程
6. GC 开销超限
错误信息:
java.lang.OutOfMemoryError: GC overhead limit exceeded原因:JVM 98% 的时间在做 GC,却只回收不到 2% 内存,进入 “累死状态”。
本质:内存快空了,又不断产生垃圾。
五、内存泄漏 vs 内存溢出
这是面试 + 实战必考点。
1. 内存溢出(OOM)
- 内存真的不够用了
- 一次性加载太多对象
- 加内存可以临时缓解
2. 内存泄漏(Memory Leak)
- 对象不用了,但一直被引用,GC 无法回收
- 内存慢慢被吃掉
- 最终一定会导致 OOM
- 加内存没用,必须改代码
最常见泄漏场景:
- static List/Map 不断添加对象
- ThreadLocal 没
remove() - 数据库连接、IO 流未关闭
- 内部类持有外部类引用
- 缓存不加过期、淘汰机制
六、线上 OOM 标准排查流程
第一步:必须开启 OOM 自动 dump(重中之重)
在 JVM 启动参数里加:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/heap.hprofOOM 时会自动导出内存快照,这是定位问题的关键。
第二步:分析 dump 文件
工具:
- Eclipse MAT(最强大)
- JProfiler
- Arthas(阿里开源,线上神器)
重点看 4 点:
- 哪个类的对象最多
- 谁在引用这些对象
- 是不是业务对象(是 → 代码问题)
- GC Roots 链在哪里
第三步:配合命令行工具定位
jstat -gc 进程ID 1000 10 # 看GC情况 jmap -heap 进程ID # 看堆配置 jmap -histo 进程ID # 看对象数量 arthas-boot.jar # 阿里Arthas,一键排查判断内存泄漏最简单方式:每次 Full GC 后,内存水位不下降 → 100% 内存泄漏
七、OOM 通用解决方案
1. 代码层面(根治)
- 数据库查询必须分页
- 大文件使用流式读取,不一次性加载
- 资源用完必须关闭(连接、流、ThreadLocal)
- 慎用 static 集合
- 线程池标准化,不无限创建线程
- 缓存加过期、淘汰、降级策略
2. JVM 参数优化
-Xms2g -Xmx2g # 堆初始值和最大值 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError3. 架构层面
- 消息队列削峰
- 分布式计算,压力分散
- 大文件上传走分片
- 热点数据放缓存,不全部放内存
八、总结(一句话记住 OOM)
- OOM =内存满了,新对象没地方分配
- Java heap space最常见,大多是没分页、内存泄漏
- Metaspace溢出 = 类太多
- Direct buffer溢出 = NIO/Netty 问题
- StackOverflow= 递归 / 调用太深
- 无法创建线程= 线程太多
- 排查核心:dump 文件 + MAT/Arthas
- 90% OOM 都是代码问题,不是 JVM 内存太小
