穩定性專題 | StackOverFlowError 常見緣由及解決方法

導讀

『StabilityGuide』是阿里多位阿里技術工程師共同發起的穩定性領域的知識庫開源項目,涵蓋性能壓測、故障演練、JVM、應用容器、服務框架、流量調度、監控、診斷等多個技術領域,以更結構化的方式來打造穩定性領域的知識庫,歡迎您的加入。html

@GitHub :https://github.com/StabilityMan/StabilityGuidejava

每個 JVM 線程都擁有一個私有的 JVM 線程棧,用於存放當前線程的 JVM 棧幀(包括被調用函數的參數、局部變量和返回地址等)。若是某個線程的線程棧空間被耗盡,沒有足夠資源分配給新建立的棧幀,就會拋出 java.lang.StackOverflowError 錯誤。git

線程棧是如何運行的?

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

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() 方法入棧。oracle

而後,a() 方法入棧,變量 x 被聲明爲 int 類型,初始化賦值爲 0。注意,不管是 x 仍是 0 都被包含在棧幀中。框架

接着,b() 方法入棧,建立了一個 Car 對象,並被賦給變量 y。請注意,實際的 Car 對象是在 Java 堆內存中建立的,而不是線程棧中,只有 Car 對象的引用以及變量 y 被包含在棧幀裏。jvm

最後,c() 方法入棧,變量 z 被聲明爲 float 類型,初始化賦值爲 0f。同理,z 仍是 0f 都被包含在棧幀裏。ide

當方法執行完成後,全部的線程棧幀將按照後進先出的順序逐一出棧,直至棧空爲止。函數

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 版本和供應商,常見的默認配置以下表所示:

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 1024 kb
Windows 32-bit JVM 320 kb
Windows 64-bit JVM 1024 kb

提示: 實際生產系統中,能夠對程序日誌中的 StackOverFlowError 配置關鍵字告警,一經發現,當即處理。

推薦工具&產品

ARMS —— 阿里雲 APM 產品,支持 StackOverFlowError 異常關鍵字告警

參考文章

做者信息:夏明,GitHub ID @StabilityMan,花名涯海,阿里雲 ARMS & EagleEye 技術專家,2016 年加入阿里巴巴,一直從事鏈路追蹤和 APM 監控診斷領域的相關工做。

 

原文連接

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索