文章出處

概述

之所以想寫這篇文章,其實是因為最近有不少系統出現了棧溢出導致進程crash的問題,并且很隱蔽,根本原因還得借助coredump才能分析出來,于是想從JVM實現的角度來全面分析下棧溢出的這類問題,或許你碰到過如下的場景:

  • 日志里出現了StackOverflowError的異常

  • 進程突然消失了,但是留下了crash日志

  • 進程消失了,crash日志也沒有留下

這些都可能是棧溢出導致的。

如何定位是否是棧溢出

上面提到的后面兩種情況有可能不是我們今天要聊的棧溢出的問題導致的crash,也許是別的一些可能,那如何確定上面三種情況是棧溢出導致的呢?

  • 出現了StackOverflowError,這種毫無疑問,必然是棧溢出,具體什么方法導致的棧溢出從棧上是能知道的,不過要提醒一點,我們打印出來看到的棧可能是不全的,因為JVM里對棧的輸出條數是可以控制的,默認是1024,這個參數是-XX:MaxJavaStackTraceDepth=1024,可以將這個參數設置為-1,那將會全部輸出對應的堆棧

  • 如果進程消失了,但是留下了crash日志,那請檢查下crash日志里的Current thread的stack范圍,以及RSP寄存器的值,如果RSP寄存器的值是超出這個stack范圍的,那說明是棧溢出了。

  • 如果crash日志也沒有留下,那只能通過coredump來分析了,在進程運行前,先執行ulimit -c unlimited,然后再跑進程,在進程掛掉之后,會產生一個core.<pid>的文件,然后再通過jstack $JAVA_HOME/bin/java core.<pid>來看輸出的棧,如果正常輸出了,那就可以看是否存在很長的調用棧的線程,當然還有可能沒有正常輸出的,因為jstack的這條從core文件抓棧的命令其實是基于serviceability agent來實現的,而SA在某些版本里是存在bug的,當然現在的SA也不能說完全沒有bug,還是存在不少bug的,祝你好運。

如何解決棧溢出的問題

這個需要具體問題具體分析,因為導致棧溢出的原因很多,提三個主要的: * java代碼寫得不當,比如出現遞歸死循環,這也是最常見的,只能靠寫代碼的人稍微小心了 * native代碼有棧上分配的邏輯,并且要求的內存還不小 * 線程棧空間設置比較小

有時候我們的代碼需要調用到native里去,最常見的一種情況譬如java.net.SocketInputStream.read0方法,這是一個native方法,在進入到這個方法里之后,它首先就要求到棧上去分配一個64KB的緩存(64位linux),試想一下如果執行到read0這個方法的時候,剩余的棧空間已經不足以分配64KB的內存了會怎樣?也許就是一開頭我們提到的crash,這只是一個例子,還有其他的一些native實現,包括我們自己也可能寫這種native代碼,如果真有這種情況,我們就需要好好斟酌下我們的線程棧到底要設置多大了。

如果我們的代碼確實存在正常的很深的遞歸調用的話,通常是我們的棧可能設置太小,我們可以通過-Xss或者-XX:ThreadStackSize來設置java線程棧的大小,如果兩個參數都設置了,那具體有效的是寫在后面的那個生效。順便提下,線程棧內存是和java heap獨立的內存,并不是在java heap內分配的,是直接malloc分配的內存。

線程棧大小

在jvm里,線程其實不僅僅只有一種,比如我們java里創建的叫做java線程,還有gc線程,編譯線程等,默認情況下他們的棧大小如下:

 

 

 

可見默認情況下編譯線程需要的棧空間是其他種類線程的4倍。

各種類型的線程他們所需要的棧的大小其實是可以通過不同的參數來控制的:

 

  • java_thread的stack_size,其實就是-Xss或者-XX:ThreadStackSize的值

  • compiler_thread的stack_size,是-XX:CompilerThreadStackSize指定的值

  • vm內部的線程比如gc線程等可以通過-XX:VMThreadStackSize來設置

JVM里棧溢出的實現

JVM里的棧溢出到底是怎么實現的,得從棧的大致結構說起:

 

會預留兩塊受保護的內存區域,分別叫做yellow page和red page,其中yellow page在前,另外如果是java創建的線程,最后并沒有圖示的一個page的glibc guard page,非java線程是有的,但是沒有yellow和red page,比如我們的gc線程,注意編譯線程其實是java線程。

除了yellow page和red page,其實還有個shadow page,這三個page可以分別通過vm參數-XX:StackYellowPages,-XX:StackRedPages,-XX:StackShadowPages來控制。當我們要調用某個java方法的時候,它需要多大的棧其實是預先知道的,javac里就計算好了,但是如果調用的是native方法,那這就不好辦了,在native方法里到底需要多大內存,這個無法得知,因此shadow page就是用來做一個大致的預測,看需要多大的棧空間,如果預測到新的RSP的值超過了yellowpage的位置,那就直接拋出棧溢出的異常,否則就去新的方法里處理,當我們的代碼訪問到yellow page或者red page里的地址的時候,因為這塊內存是受保護的,所以會產生SIGSEGV的信號,此時會交給JVM里的信號處理函數來處理,針對yellow page以及red page會有不同的處理策略,其中yellow page的處理是會拋出StackOverflowError的異常,進程不會掛掉,也就是文章開頭提到的第一個場景,但是如果是red page,那將直接導致進程退出,不過還是會產生Crash的日志,也就是文章開頭提到的第二個場景,另外還有第三個場景,其實是沒有棧空間了并且訪問了超過了red page的地址,這個時候因為棧空間不夠了,所以信號處理函數都進不去,因此就直接crash了,crash日志也不會產生。

 

 

 

 

了解上面的場景之后,再回過頭來想想JVM為什么要設置這幾個page,其實是為了安全,能預測到棧溢出的話就拋出StackOverfolwError,而避免導致進程掛掉。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()