天天看點

java棧溢出現象_JVM源碼分析之棧溢出完全解讀

java棧溢出現象_JVM源碼分析之棧溢出完全解讀

概述

之是以想寫這篇文章,其實是因為最近有不少系統出現了棧溢出導緻程序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.的檔案,然後再通過jstack $JAVA_HOME/bin/java core.來看輸出的棧,如果正常輸出了,那就可以看是否存在很長的調用棧的線程,當然還有可能沒有正常輸出的,因為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線程,編譯線程等,預設情況下他們的棧大小如下:

size_t os::Linux::default_stack_size(os::ThreadType thr_type) {

// default stack size (compiler thread needs larger stack)

#ifdef AMD64

size_t s = (thr_type == os::compiler_thread ? 4 * M : 1 * M);

#else

size_t s = (thr_type == os::compiler_thread ? 2 * M : 512 * K);

#endif // AMD64

return s;

}複制

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

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

switch (thr_type) {

case os::java_thread:

// Java threads use ThreadStackSize which default value can be

// changed with the flag -Xss

assert (JavaThread::stack_size_at_create() > 0, "this should be set");

stack_size = JavaThread::stack_size_at_create();

break;

case os::compiler_thread:

if (CompilerThreadStackSize > 0) {

stack_size = (size_t)(CompilerThreadStackSize * K);

break;

} // else fall through:

// use VMThreadStackSize if CompilerThreadStackSize is not defined

case os::vm_thread:

case os::pgc_thread:

case os::cgc_thread:

case os::watcher_thread:

if (VMThreadStackSize > 0) stack_size = (size_t)(VMThreadStackSize * K);

break;

}複制

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

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

vm内部的線程比如gc線程等可以通過-XX:VMThreadStackSize來設定

JVM 裡棧溢出的實作

JVM 裡的棧溢出到底是怎麼實作的,得從棧的大緻結構說起:

// Java thread:

//

// Low memory addresses

// +------------------------+

// | |\ JavaThread created by VM does not have glibc

// | glibc guard page | - guard, attached Java thread usually has

// | |/ 1 page glibc guard.

// P1 +------------------------+ Thread::stack_base() - Thread::stack_size()

// | |\

// | HotSpot Guard Pages | - red and yellow pages

// | |/

// +------------------------+ JavaThread::stack_yellow_zone_base()

// | |\

// | Normal Stack | -

// | |/

// P2 +------------------------+ Thread::stack_base()

//

// Non-Java thread:

//

// Low memory addresses

// +------------------------+

// | |\

// | glibc guard page | - usually 1 page

// | |/

// P1 +------------------------+ Thread::stack_base() - Thread::stack_size()

// | |\

// | Normal Stack | -

// | |/

// P2 +------------------------+ Thread::stack_base()

//

// ** P1 (aka bottom) and size ( P2 = P1 - size) are the address and stack size returned from

// pthread_attr_getstack()複制

linux下java線程棧是從高位址往低位址方向走的,在棧尾(低位址)會預留兩塊受保護的記憶體區域,分别叫做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日志也不會産生。

if (sig == SIGSEGV) {

address addr = (address) info->si_addr;

// check if fault address is within thread stack

if (addr < thread->stack_base() &&

addr >= thread->stack_base() - thread->stack_size()) {

// stack overflow

if (thread->in_stack_yellow_zone(addr)) {

thread->disable_stack_yellow_zone();

if (thread->thread_state() == _thread_in_Java) {

// Throw a stack overflow exception. Guard pages will be reenabled

// while unwinding the stack.

stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::STACK_OVERFLOW);

} else {

// Thread was in the vm or native code. Return and try to finish.

return 1;

}

} else if (thread->in_stack_red_zone(addr)) {

// Fatal red zone violation. Disable the guard pages and fall through

// to handle_unexpected_exception way down below.

thread->disable_stack_red_zone();

tty->print_raw_cr("An irrecoverable stack overflow has occurred.");

// This is a likely cause, but hard to verify. Let's just print

// it as a hint.

tty->print_raw_cr("Please check if any of your loaded .so files has "

"enabled executable stack (see man page execstack(8))");

} else {

// Accessing stack address below sp may cause SEGV if current

// thread has MAP_GROWSDOWN stack. This should only happen when

// current thread was created by user code with MAP_GROWSDOWN flag

// and then attached to VM. See notes in os_linux.cpp.

if (thread->osthread()->expanding_stack() == 0) {

thread->osthread()->set_expanding_stack();

if (os::Linux::manually_expand_stack(thread, addr)) {

thread->osthread()->clear_expanding_stack();

return 1;

}

thread->osthread()->clear_expanding_stack();

} else {

fatal("recursive segv. expanding stack.");

}

}

}

}

......

if (stub != NULL) {

// save all thread context in case we need to restore it

if (thread != NULL) thread->set_saved_exception_pc(pc);

uc->uc_mcontext.gregs[REG_PC] = (greg_t)stub;

return true;

}

// signal-chaining

if (os::Linux::chained_handler(sig, info, ucVoid)) {

return true;

}

if (!abort_if_unrecognized) {

// caller wants another chance, so give it to him

return false;

}

if (pc == NULL && uc != NULL) {

pc = os::Linux::ucontext_get_pc(uc);

}

// unmask current signal

sigset_t newset;

sigemptyset(&newset);

sigaddset(&newset, sig);

sigprocmask(SIG_UNBLOCK, &newset, NULL);

VMError err(t, sig, pc, info, ucVoid);

err.report_and_die();

ShouldNotReachHere();複制

了解上面的場景之後,再回過頭來想想JVM為什麼要設定這幾個page,其實是為了安全,能預測到棧溢出的話就抛出StackOverfolwError,而避免導緻程序挂掉。