OutOfMemoryError 有哪几种?分别是什么原因导致的?
简化版
OutOfMemoryError 不止「堆满了」一种,常见的有:① Java heap space(堆溢出)——堆里对象太多放不下,最常见,原因是内存泄漏、堆设置太小、或一次性加载太多数据;② GC overhead limit exceeded——GC 花了 98% 以上的时间却只回收了 <2% 的内存,JVM 认为「GC 快累死了还没用」提前抛出,本质也是堆快满了;③ Metaspace(元空间溢出)——加载的类太多,元空间(存类元数据)放不下,常见于动态生成大量类(CGLIB、频繁热部署导致类加载器泄漏);④ unable to create new native thread——创建的线程太多,操作系统不给分配了(每个线程要占栈内存),原因是线程数失控(没用线程池、线程泄漏);⑤ Direct buffer memory(堆外内存溢出)——NIO 的 DirectByteBuffer 分配的堆外内存超限;⑥ Requested array size exceeds VM limit——申请的数组超过 JVM 能表示的最大长度。不同 OOM 病因和排查方法完全不同,第一步是看清 OOM 的具体类型(错误信息里的那句话)。
详细版
各类 OOM 对比:
| OOM 类型 | 哪块内存 | 常见原因 | 排查/解决 |
|---|---|---|---|
| Java heap space | 堆 | 内存泄漏 / 堆太小 / 一次加载太多数据 | dump 堆用 MAT 分析;调大 -Xmx |
| GC overhead limit exceeded | 堆 | 堆快满,GC 徒劳 | 同上(本质是堆的问题) |
| Metaspace | 元空间 | 类太多 / 动态生成类 / 类加载器泄漏 | 调大 MaxMetaspaceSize;查类加载器泄漏 |
| unable to create new native thread | 栈/OS | 线程太多 | 用线程池;查线程泄漏;调 OS 限制 |
| Direct buffer memory | 堆外 | DirectByteBuffer 分配超限 | 调 MaxDirectMemorySize;查堆外泄漏 |
| Requested array size exceeds VM limit | 堆 | 数组超最大长度 | 检查数组大小逻辑 |
// ① 堆溢出:不断往集合塞对象且不释放
List<byte[]> list = new ArrayList<>();
while (true) list.add(new byte[1024 * 1024]); // Java heap space
// ③ 元空间溢出:动态生成大量类(每个 CGLIB 代理是一个新类)
while (true) { generateNewClassWithCGLIB(); } // Metaspace
// ④ 线程溢出:无限创建线程
while (true) new Thread(() -> sleep()).start(); // unable to create new native thread
⚠️ 看到 OOM 别急着调大 -Xmx——先看是哪种 OOM。如果是「Java heap space」且是内存泄漏,调大堆只是让它晚点崩溃(治标不治本,泄漏的对象还在积累);如果是「Metaspace」,调 -Xmx 根本没用(要调 MaxMetaspaceSize 且查类加载器泄漏);如果是「unable to create new native thread」,调大堆反而更糟(堆越大,留给线程栈的原生内存越少)。正确姿势:先读错误信息确定 OOM 类型 → 对症下药(堆溢出 dump 分析、元空间查类加载器、线程 OOM 查线程数)。
完整版教学
一、OOM 不止一种:先看类型
面试和排查的第一原则:OOM 有很多种,先看清是哪一种:
OutOfMemoryError 的完整错误信息会告诉你类型:
java.lang.OutOfMemoryError: Java heap space → 堆
java.lang.OutOfMemoryError: GC overhead limit exceeded → 堆(GC 徒劳)
java.lang.OutOfMemoryError: Metaspace → 元空间
java.lang.OutOfMemoryError: unable to create new native thread → 线程
java.lang.OutOfMemoryError: Direct buffer memory → 堆外内存
java.lang.OutOfMemoryError: Requested array size exceeds VM limit → 数组
★ 不同类型 = 不同内存区域 = 不同病因 = 不同解法
第一步永远是:读错误信息,确定类型!
这是排查 OOM 的「总纲」——OutOfMemoryError 是个大类,冒号后面的那句话才告诉你「哪块内存满了」。堆、元空间、线程栈、堆外内存是完全不同的内存区域,病因和解法天差地别。很多人一看到 OOM 就调 -Xmx,如果是元空间或线程 OOM 就完全无效。理解「OOM 分很多种、错误信息的冒号后指明类型、先确定类型再对症」,就掌握了排查 OOM 的第一步。
二、Java heap space:最常见的堆溢出
Java heap space 是最常见的 OOM——堆里放不下对象了:
原因(三类):
① 内存泄漏:对象一直被引用、回收不掉,越积越多(最需警惕)
- 如静态集合一直往里加、监听器没注销、ThreadLocal 没 remove
② 堆设置太小:业务确实需要这么多内存,但 -Xmx 给小了
③ 一次性加载太多数据:一次查几百万行、读大文件到内存
排查:
1. -XX:+HeapDumpOnOutOfMemoryError 自动 dump 堆
2. 用 MAT(Eclipse Memory Analyzer)分析 dump
3. 看"哪些对象占了大部分堆"(Dominator Tree)
4. 看"谁引用着它们"(判断是泄漏还是正常)
堆溢出要区分「泄漏」还是「真的需要这么多」:泄漏是对象该回收却回收不掉(越积越多,调大堆只是推迟崩溃),要 dump 堆用 MAT 找「谁一直引用着大量对象」;真需要是业务数据量大,调大 -Xmx 或优化(分批处理、流式读取)。关键工具:-XX:+HeapDumpOnOutOfMemoryError(OOM 时自动 dump)+ MAT 分析。理解「Java heap space 分泄漏/堆太小/一次加载太多、dump 堆用 MAT 分析、泄漏调大堆只是推迟」,就掌握了最常见 OOM 的排查。
三、GC overhead limit exceeded:GC 徒劳
GC overhead limit exceeded 是「堆快满了、GC 拼命但徒劳」的信号:
触发条件(默认):
连续多次 GC,GC 花了 >98% 的时间,却只回收了 <2% 的堆
→ JVM 判断:"再 GC 也没用了,直接抛 OOM 提前止损"
为什么有这个机制:
如果没有它,堆快满时会陷入"疯狂 GC → 只回收一点点 → 立刻又满 → 再 GC"
的死循环,应用假死(CPU 100% 都在 GC,但没崩溃)
→ 这个机制让它"早点崩溃",避免长时间假死
本质:还是堆的问题(内存泄漏或堆太小),只是 JVM 提前抛出
处理:和 Java heap space 一样(dump 堆分析)
也可 -XX:-UseGCOverheadLimit 禁用(但只是掩盖问题,不推荐)
这个 OOM 的本质还是堆满了——只是 JVM 发现「GC 花了 98% 时间只回收 2% 内存」,认为「继续 GC 是徒劳的」,提前抛 OOM 避免应用长时间「假死」(CPU 全在 GC 但不崩溃)。它是一种「保护机制」。处理方法和 Java heap space 一样(dump 堆找泄漏或调大堆)。理解「GC overhead limit exceeded = GC 花 98% 时间只回收 2%、是堆满的保护机制避免假死、本质还是堆问题」,就理解了这个容易误解的 OOM。
四、Metaspace:类太多的元空间溢出
Metaspace OOM 是「加载的类太多」——和堆无关:
Metaspace(元空间)存什么:类的元数据(类的结构、方法、字段信息)
JDK 8 起,类元数据从永久代(PermGen)移到元空间(用本地内存)
Metaspace 溢出的原因:
① 加载的类太多(正常情况罕见)
② 动态生成大量类:CGLIB 代理、动态代理、脚本引擎(每次生成一个新类)
③ 类加载器泄漏:频繁热部署,旧类加载器没被 GC
→ 它加载的类也无法卸载,元空间越积越多(最隐蔽)
排查:
- 调大 -XX:MaxMetaspaceSize(治标)
- 查是不是动态生成类失控 或 类加载器泄漏(治本)
- -XX:+HeapDumpOnOutOfMemoryError 也能 dump,看类加载器数量
Metaspace OOM 的关键是「类太多」,而调 -Xmx 完全无效(元空间是独立的本地内存)。最隐蔽的原因是类加载器泄漏:频繁热部署时,旧类加载器如果被强引用没被 GC,它加载的所有类都无法卸载,元空间不断增长。要查「是否动态生成类失控(CGLIB)或类加载器泄漏」。理解「Metaspace 存类元数据、溢出因类太多(动态生成类/类加载器泄漏)、调 -Xmx 无效要调 MaxMetaspaceSize 并查类加载器泄漏」,就掌握了元空间 OOM。
五、unable to create new native thread:线程太多
unable to create new native thread 是「创建的线程太多」:
原因:每个 Java 线程对应一个 OS 线程,要占用:
- 线程栈内存(默认约 1M,-Xss 配置)
- OS 的线程资源(有上限)
当线程数太多,OS 拒绝创建新线程 → 这个 OOM
常见原因:
① 没用线程池,每个任务 new Thread(线程数失控)
② 线程池配置不当,maxPoolSize 无上限
③ 线程泄漏:线程创建后没结束(如死循环、阻塞不退出)
一个反直觉的点:
堆(-Xmx)设太大 → 留给线程栈的本地内存变少 → 更容易触发这个 OOM
(进程总内存有限,堆占多了,线程栈就少了)
排查:jstack 看线程数和线程都在干嘛;用线程池;查线程泄漏
这个 OOM 和堆无关——是线程数太多,OS 不给创建了(每个线程要占栈内存 + OS 资源)。原因通常是「没用线程池」或「线程泄漏」。一个反直觉的点:堆 -Xmx 设太大反而更容易触发它(进程总内存有限,堆占多了留给线程栈的本地内存就少了)。排查用 jstack 看线程数和状态,解决靠线程池 + 查泄漏。理解「线程 OOM 是线程数太多 OS 拒绝、原因是没用线程池/线程泄漏、堆设太大反而更容易触发」,就掌握了线程 OOM。
六、其余类型与排查总纲
补上其余 OOM 类型,并总结排查总纲:
⑤ Direct buffer memory(堆外内存溢出):
NIO 的 DirectByteBuffer 分配的堆外内存超过 -XX:MaxDirectMemorySize
原因:Netty 等大量用堆外内存、或 DirectByteBuffer 泄漏(Cleaner 没触发)
→ 调 MaxDirectMemorySize,查堆外内存泄漏
⑥ Requested array size exceeds VM limit:
申请的数组长度超过 JVM 能表示的最大值(约 Integer.MAX_VALUE-2)
→ 检查是不是数组大小计算有 bug
排查总纲:
第1步:读错误信息 → 确定 OOM 类型(哪块内存)
第2步:对症下药
堆(heap space / GC overhead)→ dump 堆用 MAT,区分泄漏/堆太小
元空间(Metaspace)→ 查动态生成类 / 类加载器泄漏
线程(native thread)→ jstack 看线程数,用线程池
堆外(Direct buffer)→ 查堆外泄漏,调 MaxDirectMemorySize
排查 OOM 的「总纲」是:先读错误信息确定类型(哪块内存满了),再对症下药——堆问题 dump 堆用 MAT、元空间查类加载器、线程 OOM 用 jstack、堆外查泄漏。切忌「一看 OOM 就调 -Xmx」(对元空间、线程 OOM 完全无效甚至有害)。理解「堆外/数组 OOM 的原因、以及’先确定类型再对症’的排查总纲」,就完整掌握了 OOM 的排查方法论。
记忆钩子:「OOM 六类:① Java heap space(堆满,最常见,泄漏/堆小/一次加载太多,dump 堆用 MAT)② GC overhead limit exceeded(GC 花 98% 时间回收<2%,堆满的保护机制,本质还是堆)③ Metaspace(类太多,动态生成类/类加载器泄漏,调 -Xmx 无效)④ unable to create new native thread(线程太多 OS 拒绝,用线程池,堆大反而更容易触发)⑤ Direct buffer memory(堆外超限)⑥ 数组超限;排查总纲:先读错误信息确定类型→对症下药,别一律调 -Xmx」。
七、常见误区与追问
- 误区:OOM 就是堆满了,调大 -Xmx 就行。 OOM 有很多种——元空间 OOM、线程 OOM、堆外 OOM 调 -Xmx 完全无效;即使是堆 OOM,若是内存泄漏,调大堆只是推迟崩溃。第一步永远是读错误信息确定类型。
- 误区:GC overhead limit exceeded 是 GC 本身有 bug。 不是——它是 JVM 的保护机制:GC 花 98% 时间只回收 2% 内存时提前抛 OOM,避免应用长时间「疯狂 GC 但不崩溃」的假死;本质还是堆快满了(泄漏或太小)。
- 误区:Metaspace OOM 调大 -Xmx 能解决。 完全无效——元空间是独立的本地内存,要调 -XX:MaxMetaspaceSize;且要查根因(动态生成类失控、类加载器泄漏),否则调大也会再次溢出。
- 误区:把 -Xmx 设得越大越安全。 堆设太大会挤占留给线程栈、堆外内存的本地内存,反而更容易触发「unable to create new native thread」或堆外 OOM;进程总内存是有限的,要平衡各区域。
- 追问:怎么区分堆 OOM 是内存泄漏还是堆太小? dump 堆用 MAT 看 Dominator Tree——如果是少数对象/集合占了绝大部分堆且随时间单调增长,多半是泄漏(找「谁引用着它们」);如果是业务数据均匀分布、符合预期,就是堆太小(调大 -Xmx 或分批处理)。
- 追问:Metaspace 溢出为什么和类加载器泄漏有关? 类的元数据由加载它的类加载器持有;类加载器只有失去引用被 GC,它加载的类才能卸载、释放元空间。频繁热部署时旧类加载器若被强引用(如静态字段、线程持有),它加载的类无法卸载,元空间不断增长。
- 追问:unable to create new native thread 为什么和堆大小有关? 进程可用内存总量有限(尤其 32 位或有 cgroup 限制),分给堆的多了,剩给线程栈(每个约 1M 本地内存)的就少了;线程数一多就没内存创建新线程栈了,所以堆设太大反而更容易触发线程 OOM。
八、加强记忆
OutOfMemoryError 不止「堆满」一种,排查第一步永远是读错误信息确定类型。六类:① Java heap space(堆满,最常见——内存泄漏/堆太小/一次加载太多数据,dump 堆用 MAT 区分泄漏还是堆小);② GC overhead limit exceeded(GC 花 98% 时间只回收 <2%,是堆满的保护机制避免疯狂 GC 假死,本质还是堆问题);③ Metaspace(类太多——动态生成类(CGLIB)/类加载器泄漏(频繁热部署),调 -Xmx 无效,要调 MaxMetaspaceSize 并查类加载器泄漏);④ unable to create new native thread(线程太多 OS 拒绝——没用线程池/线程泄漏,堆设太大反而更容易触发,用 jstack 查);⑤ Direct buffer memory(堆外内存 DirectByteBuffer 超限,调 MaxDirectMemorySize);⑥ Requested array size exceeds VM limit(数组超最大长度)。排查总纲:先读错误信息定类型 → 对症下药,切忌一律调 -Xmx(对元空间/线程 OOM 无效甚至有害)。一句话「OOM 分六类,先读错误信息定类型再对症:堆(dump 用 MAT 分泄漏/太小)、元空间(查动态生成类和类加载器泄漏,调 -Xmx 无效)、线程(用线程池,堆大反更糟)、堆外(查泄漏调 MaxDirectMemorySize)」。