← 返回题目列表

OutOfMemoryError 有哪几种?分别是什么原因导致的?

中等 第 22 / 34 题 更新于 2026/07/28
OOM内存溢出堆溢出元空间

简化版

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)」。