閱讀785 返回首頁    go 阿裏雲 go 技術社區[雲棲]


JVM源碼分析之棧溢出完全解讀

640?wx_fmt=png&tp=webp&wxfrom=5

概述

之所以想寫這篇文章,其實是因為最近有不少係統出現了棧溢出導致進程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線程,編譯線程等,默認情況下他們的棧大小如下:

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,而避免導致進程掛掉。

最後更新:2017-04-11 19:32:01

  上一篇:go JDK8在泛型類型推導上的變化
  下一篇:go 4月11日雲棲精選夜讀:2016年Java麵試題整理