天天看點

StackOverFlowError 常見原因及解決方法

線程棧是如何運作的?

首先給出一個簡單的程式調用代碼示例,如下所示:

當 main() 方法被調用後,執行線程按照代碼執行順序,将它正在執行的方法、基本資料類型、對象指針和傳回值包裝在棧幀中,逐一壓入其私有的調用棧,整體執行過程如下圖所示:

StackOverFlowError 常見原因及解決方法

首先,程式啟動後,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 錯誤。

在這種情況下,a() 方法将無限入棧,直至棧溢出,耗盡線程棧空間,如下圖所示。

StackOverFlowError 常見原因及解決方法

如何解決 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 版本和供應商,常見的預設配置如下表所示:

jvm 版本

線程棧預設大小

sparc 32-bit jvm

512 kb

sparc 64-bit jvm

1024 kb

x86 solaris/linux 32-bit jvm

320 kb

x86 solaris/linux 64-bit jvm

windows 32-bit jvm

windows 64-bit jvm

提示: 實際生産系統中,可以對程式日志中的 stackoverflowerror 配置關鍵字告警,一經發現,立即處理。推薦工具&産品

arms —— 阿裡雲 apm 産品,支援 stackoverflowerror 異常關鍵字告警

想知道更多?長按/掃碼關注我吧↓↓↓

StackOverFlowError 常見原因及解決方法