← 返回题目列表

StackOverflowError 和 OutOfMemoryError 有什么区别?

高频 中等 第 12 / 34 题 更新于 2026/07/25
JVMOOMStackOverflowError

简化版

StackOverflowError单个线程的调用栈太深(多半是递归没出口),栈空间耗尽;OutOfMemoryErrorJVM 某块内存区域再也分不出所需空间(堆、元空间、直接内存、甚至建不出新线程)。一个是「栈太深」,一个是「内存不够」,都是 Error 不是 Exception,一般不该 catch。

详细版

StackOverflowError:每次方法调用都会压入一个栈帧(存局部变量、返回地址等)。递归没有正确终止、或调用链异常深,线程栈空间被压满,就抛 StackOverflowError。修复重点是检查递归出口、减少调用深度,而不是无脑增大 -Xss——那只是把 bug 藏得更深。

OutOfMemoryError:先看具体是哪块内存的错误信息,方向完全不同:

  • Java heap space:堆满了——对象持续累积、缓存无上限、内存泄漏;
  • Metaspace:元空间满——动态生成类过多、类加载器泄漏;
  • Direct buffer memory:NIO 直接内存(堆外)耗尽;
  • unable to create new native thread:线程数太多或系统资源受限,建不出新线程。

排查堆 OOM 要保留 heap dump-XX:+HeapDumpOnOutOfMemoryError),用 MAT 之类工具找「最大保留集」和 GC Root 引用链,看清是谁一直握着对象不放。

完整版教学

一、根本区别:栈 vs 各类内存区

要答清这题,先有 JVM 内存结构的图:

每个线程私有:  虚拟机栈(栈帧)← StackOverflowError / 栈 OOM 就发生在这
              本地方法栈、程序计数器
所有线程共享:  堆(对象)      ← OutOfMemoryError: Java heap space
              元空间(类元数据)← OutOfMemoryError: Metaspace
              (堆外)直接内存   ← OutOfMemoryError: Direct buffer memory
  • StackOverflowError:发生在线程私有的虚拟机栈。栈深度超限(-Xss 决定单线程栈大小),是「一根栈压太深」。
  • OutOfMemoryError:发生在堆/元空间/直接内存等区域,是「某块内存整体不够分了」。

补充:如果不停创建线程,每个线程都要分配栈空间,也可能抛「栈相关的 OOM」(unable to create new native thread)——这时问题不是单线程太深,而是线程太多,属于 OOM 范畴。

二、StackOverflowError 怎么排查和修

99% 是递归问题:

int factorial(int n) {
    return n * factorial(n - 1);   // ❌ 忘了 n <= 1 的出口 → 无限递归
}

排查看抛出时的堆栈——如果同一个方法名反复出现、堆栈超长,基本就是递归没出口或出口条件写错。修复:

  • 检查递归终止条件是否正确;
  • 深递归改迭代(用显式栈/循环);
  • 真是合理的深调用(少见)才考虑调大 -Xss——但先怀疑逻辑。

三、OOM 怎么排查:先定位区域,再找根因

不能一看到 OOM 就调大 -Xmx——那可能只是把爆炸推迟。正确流程:

  1. 看错误信息确定是哪块内存(heap / Metaspace / direct / thread);
  2. 堆 OOM → 分析 heap dump:MAT 找占用最大的对象、看它的 GC Root 引用链——关键问题是「它为什么还被引用、没被回收」。常见根因是静态集合只增不减、缓存无上限、监听器/连接没释放;
  3. 元空间 OOM → 查动态代理、字节码增强、类加载器是否泄漏(每次热部署都加载一份类却不卸载);
  4. 直接内存 OOM → 查 NIO DirectByteBuffer 是否用完没释放。

记忆点:OOM 修复是「消除泄漏 / 限制容量 / 分页处理」,而不是盲目调大内存。调参只能缓解容量型问题,治不了泄漏。

四、为什么它俩是 Error 不是 Exception

两者都继承自 Error,代表「JVM 层面的严重问题,应用通常无力恢复」。所以一般不该 catch——catch 了继续跑,程序多半处于不可预知的坏状态。正确做法是让它抛出、暴露、然后定位根因去修。(唯一例外是某些框架顶层会兜底记录后优雅退出,但那是特例。)

五、线程数、栈大小与总内存要一起看

单线程 StackOverflowError 通常是调用深度超过该线程可用栈;unable to create native thread 则通常是整个进程或系统无法再为新线程提供地址空间、提交内存或线程资源。

假设进程创建 500 个线程,每线程 -Xss1m,仅栈的理论上限预算就约 500 MB,还未包含本地线程结构。把 -Xss 从 1 MB 调到 2 MB,单线程可承受的调用深度可能增加,但相同总内存下可创建线程数会下降。

线程栈预算上限近似 = 线程数 × 每线程栈上限

这个乘法只是容量估算,实际提交方式和系统开销依平台/JVM 而异,却足以说明为什么不能无脑调大 -Xss

六、诊断要从错误消息定位区域

错误/现象优先检查常见修复方向
StackOverflowError重复栈帧、递归终止条件、调用环修代码,必要时评估栈深
Java heap spaceGC 后基线、heap dump 保留链消除泄漏、限流/分页、合理扩容
GC overhead limit exceededGC 时间与回收效果查存活集和分配压力
Metaspace类数量、类加载器可达链修类加载器泄漏、设置合理上限
Direct buffer memoryDirectBuffer 使用和释放限制池容量、及时关闭、核对堆外预算
unable to create native thread线程数、系统限制、总内存使用线程池、消除线程泄漏、调资源限制

发生 OOM 后,-XX:+HeapDumpOnOutOfMemoryError 对 Java 堆问题有价值,但对所有本地内存问题并不都能直接给出答案。还应保留 GC 日志、线程数、容器事件和 NMT 等证据。

Error 通常表示当前操作已无法正常完成。即使语法上能捕获,也只应在边界层做最小化记录或隔离,不能假设 JVM 整体状态足以继续承载业务。

七、常见误区与追问

  • 误区:调大 -Xss 就算修复了 StackOverflow。 若根因是无限递归或调用环,调参只会延迟失败并增加每线程内存预算。
  • 误区:所有 OOM 都能通过增大 -Xmx 解决。 元空间、直接内存、线程和本地分配失败不由 Xmx 直接控制。
  • 误区:捕获 OutOfMemoryError 后清空几个变量就能安全恢复。 触发时可能缺少执行清理所需资源,系统状态也未必可靠。
  • 追问:递归一定会 StackOverflow 吗? 深度受输入限制且栈足够时未必;Java 通常不保证尾调用优化,极深递归仍有风险。
  • 追问:为什么线程太多报 OOM 而不是 SOE? 每个线程的栈可能尚未溢出,但进程已无法为新线程分配整体资源。
  • 追问:堆 OOM 为什么要看回收后基线? 若每轮 GC 后最低占用持续上升,说明长期可达对象在累积;单看回收前峰值容易误判。
  • 追问:发生 OOM 时先做什么? 按应急预案保护服务,同时保存错误类型、GC 日志、线程与必要 dump,再依据区域定位。

八、加强记忆

用“单线程深度”区分 StackOverflowError,用“某类内存或系统资源无法满足申请”理解 OutOfMemoryError。调大 Xss 可能增加可承受调用深度,却会抬高每线程预算并降低可创建线程数;调大 Xmx 也只可能缓解 Java 堆容量问题。OOM 必须继续按 heap、Metaspace、direct buffer、native thread 等消息细分,并用相应日志、dump 或 NMT 验证。真正修复优先消除无限递归、泄漏和无界增长,捕获 Error 或盲目扩容都不能替代根因治理。