StackOverflowError 和 OutOfMemoryError 有什么区别?
简化版
StackOverflowError 是单个线程的调用栈太深(多半是递归没出口),栈空间耗尽;OutOfMemoryError 是 JVM 某块内存区域再也分不出所需空间(堆、元空间、直接内存、甚至建不出新线程)。一个是「栈太深」,一个是「内存不够」,都是 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——那可能只是把爆炸推迟。正确流程:
- 看错误信息确定是哪块内存(heap / Metaspace / direct / thread);
- 堆 OOM → 分析 heap dump:MAT 找占用最大的对象、看它的 GC Root 引用链——关键问题是「它为什么还被引用、没被回收」。常见根因是静态集合只增不减、缓存无上限、监听器/连接没释放;
- 元空间 OOM → 查动态代理、字节码增强、类加载器是否泄漏(每次热部署都加载一份类却不卸载);
- 直接内存 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 space | GC 后基线、heap dump 保留链 | 消除泄漏、限流/分页、合理扩容 |
GC overhead limit exceeded | GC 时间与回收效果 | 查存活集和分配压力 |
Metaspace | 类数量、类加载器可达链 | 修类加载器泄漏、设置合理上限 |
Direct buffer memory | DirectBuffer 使用和释放 | 限制池容量、及时关闭、核对堆外预算 |
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 或盲目扩容都不能替代根因治理。