一、什么是 StackOverflowError?
java.lang.StackOverflowError 是 JVM 抛出的错误(Error) ,不是普通异常(Exception)。它表示:当前线程的虚拟机栈空间被耗尽,无法再分配新的栈帧。
核心原理
- 每个线程有独立的虚拟机栈,默认大小约 1MB(可通过
-Xss调整)。 - 每调用一个方法,JVM 会创建一个栈帧压入栈中,存放局部变量、返回地址、操作数栈等。
- 方法执行完毕,栈帧出栈。
- 当压栈速度远大于出栈,栈空间被占满,就抛出 StackOverflowError。
一句话总结:方法调用层级太深,栈被撑爆了。
二、栈溢出的 4 种典型场景(附代码)
1. 无限递归(最常见)
没有正确的递归出口,方法无限调用自身。
java
运行
public class StackOverflowDemo {
public static void recursion() {
recursion(); // 无终止条件
}
public static void main(String[] args) {
recursion();
}
}
报错:直接抛出 StackOverflowError,堆栈信息疯狂重复同一行。
2. 递归深度过大(有出口但太深)
java
运行
public static int deepRecursion(int n) {
if (n == 0) return 0;
return deepRecursion(n - 1);
}
// 调用:deepRecursion(100000) → 栈溢出
默认栈深度大约在 1000~2000 层,超过就会爆。
3. 方法循环调用(A→B→A→B…)
java
运行
public class CycleCall {
public static void a() { b(); }
public static void b() { a(); }
public static void main(String[] args) { a(); }
}
本质和无限递归一样,形成调用环。
4. 超大局部变量(少见但致命)
栈不仅存调用链,还存局部变量。一次性在栈上分配超大数组会直接爆栈。
java
运行
public static void bigLocalVar() {
int[] huge = new int[1024 * 1024]; // 栈上分配,直接溢出
}
注意:对象实例在堆,数组引用在栈,但超大基本类型数组会占满栈空间。
三、关键知识点:Error vs Exception
- Exception:可捕获、可恢复(空指针、下标越界等)。
- Error:系统级错误,不建议捕获,捕获也无法恢复。
- StackOverflowError 一旦发生,线程已无法正常执行,catch 住也没意义。
四、如何快速定位 StackOverflowError?
1. 看异常堆栈(最直接)
堆栈信息会大量重复某几行,那就是递归 / 循环调用的根源。示例:
plaintext
Exception in thread "main" java.lang.StackOverflowError
at com.demo.StackOverflowDemo.recursion(StackOverflowDemo.java:5)
at com.demo.StackOverflowDemo.recursion(StackOverflowDemo.java:5)
at com.demo.StackOverflowDemo.recursion(StackOverflowDemo.java:5)
...
2. 用 jstack 抓线程栈
bash
运行
jstack -l <pid>
找到 RUNNABLE 且栈超长的线程,定位问题方法。
3. IDE 断点调试
在可疑递归 / 循环处打断点,观察调用次数与条件是否正常收敛。
五、解决方案:从根本到临时
1. 修复递归(首选)
- 必须有明确终止条件
- 每次递归必须向终止条件逼近
java
运行
// 正确递归示例
public static int factorial(int n) {
if (n <= 1) return 1; // 出口
return n * factorial(n - 1); // 收敛
}
2. 递归改迭代 / 循环(根治深度问题)
用循环 + 集合模拟栈,彻底摆脱栈深度限制。
java
运行
// 迭代替代递归
public static int loopFactorial(int n) {
int res = 1;
for (int i = 2; i <= n; i++) res *= i;
return res;
}
3. 增大栈空间(临时方案)
通过 JVM 参数 -Xss 调整线程栈大小:
plaintext
-Xss256k
-Xss1m
-Xss2m
不建议滥用:栈过大会导致线程数减少,引发 OOM,只能作为临时兼容。
4. 解除循环调用
梳理依赖关系,避免 A↔B 无限调用。
5. 大对象移到堆
把栈上超大局部变量改成对象,分配到堆。
六、栈溢出 vs 堆溢出(OOM)
表格
| 对比项 | StackOverflowError | OutOfMemoryError |
|---|---|---|
| 发生区域 | 虚拟机栈 | 堆 / 方法区 / 直接内存 |
| 原因 | 方法调用太深、栈帧太多 | 堆内存不足,无法分配对象 |
| 触发场景 | 递归、循环调用 | 内存泄漏、大对象、加载类过多 |
| JVM 参数 | -Xss | -Xms -Xmx |
七、企业级避坑建议
- 慎用递归:能用迭代就不用递归。
- 递归必加出口与深度限制。
- 禁止在栈上声明超大数组。
- 框架使用注意:MyBatis 嵌套查询、Spring AOP 嵌套切面、Lombok 构造循环都可能隐式递归。
- 上线前压测:极端数据(深度树、超长链)必测。
八、总结
StackOverflowError 本质就是栈空间被方法调用占满。
- 90% 是递归问题
- 8% 是循环调用
- 2% 是栈配置 / 局部变量问题
解决思路:先看堆栈定位重复代码 → 修复递归 / 解除循环 → 必要时调整 -Xss。
只要掌握原理与排查流程,栈溢出就是最容易解决的错误之一。