一句話先說結論:
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)
at com.example.Factorial.factorial(Factorial.java:7)
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)接不住,別試圖靠捕獲兜底。
