每一個 JVM 線程都擁有一個私有的 JVM 線程棧,用於存放當前線程的 JVM 棧幀(包括被調用函數的參數、局部變數和返回地址等)。如果某個線程的線程棧空間被耗盡,沒有足夠資源分配給新創建的棧幀,就會拋出 `
java.lang.StackOverflowError` 錯誤。本文總結了 StackOverflowError 常見原因及其解決方法,如有遺漏或錯誤,歡迎補充指正。
線程棧是如何運行的?
首先給出一個簡單的程序調用代碼示例,如下所示:
public class SimpleExample {
public static void main(String args[]) {
a();
}
public static void a() {
int x = 0;
b();
}
public static void b() {
Car y = new Car();
c();
}
public static void c() {
float z = 0f;
}
}當 `main()` 方法被調用後,執行線程按照代碼執行順序,將它正在執行的方法、基本數據類型、對象指針和返回值包裝在棧幀中,逐一壓入其私有的調用棧,整體執行過程如下圖所示:

- 首先,程序啟動後,`main()` 方法入棧。
- 然後,`a()` 方法入棧,變數 `x` 被聲明為 `int` 類型,初始化賦值為 `0`。注意,無論是 `x` 還是 `0` 都被包含在棧幀中。
- 接著,`b()` 方法入棧,創建了一個 `Car` 對象,並被賦給變數 `y`。請注意,實際的 `Car` 對象是在 Java 堆內存中創建的,而不是線程棧中,只有 `Car` 對象的引用以及變數 `y` 被包含在棧幀里。
- 最後,`c()` 方法入棧,變數 `z` 被聲明為 `float` 類型,初始化賦值為 `0f`。同理,`z` 還是 `0f` 都被包含在棧幀里。
當方法執行完成後,所有的線程棧幀將按照後進先出的順序逐一出棧,直至棧空為止。
StackOverFlowError 是如何產生的?
如上所述,JVM 線程棧存儲了方法的執行過程、基本數據類型、局部變數、對象指針和返回值等信息,這些都需要消耗內存。一旦線程棧的大小增長超過了允許的內存限制,就會拋出 `
java.lang.StackOverflowError` 錯誤。
下面這段代碼通過無限遞歸調用最終引發了 `
java.lang.StackOverflowError` 錯誤。
public class StackOverflowErrorExample {
public static void main(String args[]) {
a();
}
public static void a() {
a();
}
}在這種情況下,`a()` 方法將無限入棧,直至棧溢出,耗盡線程棧空間,如下圖所示。
Exception in thread "main" java.lang.StackOverflowError
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
at StackOverflowErrorExample.a(StackOverflowErrorExample.java:10)
如何解決 StackOverFlowError?
引發 `StackOverFlowError` 的常見原因有以下幾種:
- 無限遞歸循環調用(最常見)。
- 執行了大量方法,導致線程棧空間耗盡。
- 方法內聲明了海量的局部變數。
- native 代碼有棧上分配的邏輯,並且要求的內存還不小,比如 `java.net.SocketInputStream.read0` 會在棧上要求分配一個 64KB 的緩存(64位 Linux)。
除了程序拋出 `StackOverflowError` 錯誤以外,還有兩種定位棧溢出的方法:
- 進程突然消失,但是留下了 crash 日誌,可以檢查 crash 日誌里當前線程的 stack 範圍,以及 RSP 寄存器的值。如果 RSP 寄存器的值超出這個 stack 範圍,那就說明是棧溢出了。
- 如果沒有 crash 日誌,那隻能通過 coredump 進行分析。在進程運行前,先執行 `ulimit -c unlimited`,當進程掛掉之後,會產生一個 core.[pid] 的文件,然後再通過 `jstack $JAVA_HOME/bin/java core.[pid]` 來看輸出的棧。如果正常輸出了,那就可以看是否存在很長的調用棧的線程,當然還有可能沒有正常輸出的,因為 jstack 的這條從 core 文件抓棧的命令其實是基於 Serviceability Agent 實現的,而 SA 在某些版本里有 Bug。
常見的解決方法包括以下幾種:
- 修復引發無限遞歸調用的異常代碼, 通過程序拋出的異常堆棧,找出不斷重複的代碼行,按圖索驥,修復無限遞歸 Bug。
- 排查是否存在類之間的循環依賴。
- 排查是否存在在一個類中對當前類進行實例化,並作為該類的實例變數。
- 通過 JVM 啟動參數 `-Xss` 增加線程棧內存空間,某些正常使用場景需要執行大量方法或包含大量局部變數,這時可以適當地提高線程棧空間限制,例如通過配置 `-Xss2m` 將線程棧空間調整為 2 mb。
線程棧的默認大小依賴於操作系統、JVM 版本和供應商,常見的默認配置如下表所示:

提示: 實際生產系統中,可以對程序日誌中的 StackOverFlowError 配置關鍵字告警,一經發現,立即處理。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/268410.html
微信掃一掃
支付寶掃一掃