一句话先说结论:
StackOverflowError不是内存不够,是方法调用一层套一层,栈帧把线程栈顶穿了。最常见的根因是递归没有真出口,或者出口条件写错。先闷头看递归出口,能改循环就改循环,-Xss调大只是应急,治不了无限递归。
背景
每个线程启动时,JVM 给它划一块固定大小的栈内存。方法每被调用一次,就在栈上压一个栈帧,帧里放局部变量、返回地址、操作数。方法返回了,帧才弹掉。
普通程序调用链就三四层,这点栈绰绰有余。递归不一样,每深入一层就多一个帧,深度上去了,栈迟早见底。
栈的默认大小各平台不一样:Linux 和 macOS 主线程约 8 MB,Windows 约 1 MB。这个上限写死,不会像堆一样自动扩容。调用深度一超,JVM 直接抛 StackOverflowError。
现象
报错原文最显眼的特征是:同一行反复出现。
Exception in thread "main" java.lang.StackOverflowError
at com.example.Factorial.factorial(Factorial.java:7)
at com.example.Factorial.factorial(Factorial.java:7)
at com.example.Factorial.factorial(Factorial.java:7)
...
堆栈里同一个方法压了一长串,把「罪魁祸首」直接写在脸上。看最后几行就知道是哪个方法在疯狂调用自己。
触发条件很简单,一个没有出口的递归就够:
public static int factorial(int n) {
return n * factorial(n - 1); // 永远到不了出口
}
调 factorial(5),帧一直压,压到栈底就崩。这是最典型的一类,另一类更隐蔽,下面讲根因时细说。
根因分析
StackOverflowError 的根因都能归结成一句话:递归没有真正往下走,或者压根不终止。拆开看有四种常见形态。
第一,递归出口写错或者没写。前面 factorial 就是,没有 if (n <= 1) return 1 这一层兜底。
第二,数据结构里藏了循环引用。两个对象的 toString() 互相调用,或者 A 的某字段指向 B、B 又指向 A,序列化或打印时死循环套娃:
class A {
B b;
public String toString() { return String.valueOf(b); }
}
class B {
A a;
public String toString() { return String.valueOf(a); }
}
第三,JSON 序列化撞上循环引用,jackson、fastjson 这类库递归遍历对象图时爆栈。
第四,代理链写错。Spring AOP 里一个方法切到了自己,或者在 equals / hashCode 里又间接调了自己,形成自调用环。
一句话记住:栈溢出的本质是「调用深度失控」,不是「数据量大」。
解决方案
先修递归出口,这是根
看到 StackOverflowError,第一件事不是调参数,是按住报错堆栈最后几行,找到那个反复出现的方法,检查它到底能不能停。
递归出口必须满足两条:能命中,且每次调用都朝出口靠近。
public static int factorial(int n) {
if (n <= 1) {
return 1; // 出口,朝它收敛
}
return n * factorial(n - 1);
}
出口条件写错是最常见的坑。比如传进去的参数是对象,每次调用又新建一个没衰减的对象,深度永远不减。
改循环,最稳
递归能改循环就改循环。用显式的栈结构替代调用栈,深度不再受线程栈限制:
public static int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
树的遍历这类递归,改成 Deque 加 while 循环,一样能做到不依赖调用栈。
处理循环引用:让序列化「看得见」环
序列化爆栈,先排查对象图里有没有环。Jackson 加 @JsonIdentityInfo 或 @JsonBackReference 打破环,fastjson 也有对应的 serializerFeature 处理循环引用。根治办法是设计上别让 model 互相持有。
-Xss 调大:只应急,不治本
线上确实需要深递归、又来不及改代码时,可以把线程栈调大:
-Xss2m
注意两点:一,它只对深度有限、确实合法的递归有效;二,无限递归调多大都会崩,反而因为每线程栈变大,多线程场景更容易把物理内存吃光。
思考过程
最有迷惑性的一点是:StackOverflowError 是 Error,不是 Exception。用 try-catch (Exception e) 是接不住它的,别指望靠捕获来「兜住」爆栈。它的设计语义就是「这个错不该由业务代码处理」。
还有一个常见误判:把栈溢出当成堆溢出。看 detail message 就行——StackOverflowError 不带 OutOfMemoryError 那串堆空间描述。两者名字里都有「溢出」,但一个说的是调用栈,一个说的是堆内存。
定位方法论一句话:让崩溃现场说话,报错堆栈里重复出现的那个方法,就是递归失控的位置。别去猜,去看。
小结
StackOverflowError是调用深度超限,不是堆内存不够。- 根因几乎都是递归不终止或出口写错,少数是循环引用和代理链写坏。
- 先查递归出口,能改循环就改循环,
-Xss只当应急。 - 它是
Error,catch (Exception)接不住,别试图靠捕获兜底。