Akira's Tech Notes

Java/JVM | GNU/Linux | Emacs/Lisp | 知的好奇心駆動

header-icon
ネイティブでない日本語で思い付くことや気になることをダラダラ書く、体裁とかは気にしない。読みづらいと感じた時に随時更新する。

[調査]JVMのスタックサイズについて

1 環境

本記事の内容は以下環境を前提としています。

  • GNU/Linux x86_64
  • OpenJDK 64-Bit 1.7.0_xx

2 JVMのスタック領域について

-Xss-XX:ThreadStackSize パラメータ値と ulimit -s リソースリミット制限値を混 乱している記事を見受けたため、HotSpotの中身を調べることにしました。

結論を先に、

  • ulimit -s のスタック最大サイズ制限値は親プロセスであるJVMランチャーのみ適用される。
  • JVMランチャーやJavaAPIから起動されたJavaスレッドのスタックサイズは -Xss-XX:ThreadStackSize の値が適用される。
  • JVMランチャーから起動されたイニシャルスレッドのスタックサイズは -Xss パラメータの み制御できる、つまり -Xss の適用範囲は -XX:ThreadStackSize より広い
  • JNI経由で外部からJVMにアタッチしたスレッドのスタックサイズはJVMの管理対象外である。

JVMスタックに関して、公式のJVMスペックドキュメント (Java SE 7 Virtual Machine Specification) は次のように記載されています。

  • 2.5.2. Java Virtual Machine Stacks

    Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at
    the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A
    Java Virtual Machine stack is analogous to the stack of a conventional language such
    as C: it holds local variables and partial results, and plays a part in method
    invocation and return. Because the Java Virtual Machine stack is never manipulated
    directly except to push and pop frames, frames may be heap allocated. The memory for
    a Java Virtual Machine stack does not need to be contiguous.
    

    ※メモ: VMスタック内のFrameはヒープ上に配置されるかも知れない。

  • 2.5.6. Native Method Stacks

    An implementation of the Java Virtual Machine may use conventional stacks,
    colloquially called "C stacks," to support native methods (methods written in a
    language other than the Java programming language). Native method stacks may also be
    used by the implementation of an interpreter for the Java Virtual Machine's
    instruction set in a language such as C. Java Virtual Machine implementations that
    cannot load native methods and that do not themselves rely on conventional stacks
    need not supply native method stacks. If supplied, native method stacks are typically
    allocated per thread when each thread is created.
    

仕様上ではJVMに Java StackNative Stack 2種類のスタックメモリが定義されています。

スタック種別 説明
Java Stack Javaコード実行時に使われるスタック
Native Stack C/C++で書かれた部分実行時に使われるスタック
  システムコール、JNI経由でC/C++ライブラリコール時に使われる

以下は、JVMのメモリ論理構成イメージです。

+----------------+--------------+-------------+------------------------------------------------------+
|                |              |             |  +-----------------+  +----------+  +-------------+  |
|     Heap       |   PermGen    | Code Cache  |  | Program Counter |  |JavaStack |  |Native Stack |  |
|                |              |             |  +-----------------+  |          |  |             |  |
+----------------+--------------+-------------+                       +----------+  +-------------+  |
                                              |                       | Frame #2 |  |             |  |
                                              |                       +----------+  +-------------+  |
                                              |  << Thread >>         | Frame #1 |  |             |  |
                                              |                       +----------+  +-------------+  |
                                              +------------------------------------------------------+

図1

理論上は Java StackNative Stack がスレッド毎に領域が確保されいます。ただし、 実際のメモリページ構成はJDKの実装に依存するものです。

次の情報によると、HotSpotの実装は Java StackNative Stack が同じメモリ領域を共 有してる。

  • Troubleshooting Guide for HotSpot VM4.1.3 Crash due to Stack Overflow

    In the HotSpot implementation, Java methods share stack frames with C/C++ native
    ★~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    code, namely user native code and the virtual machine itself. Java methods generate
    code that checks that stack space is available a fixed distance towards the end of
    the stack so that the native code can be called without exceeding the stack
    space. This distance towards the end of the stack is called “Shadow Pages.” The
    size of the shadow pages is between 3 and 20 pages, depending on the platform. This
    distance is tunable, so that applications with native code needing more than the
    default distance can increase the shadow page size. The option to increase shadow
    pages is -XX:StackShadowPages=n, where n is greater than the default stack shadow
    pages for the platform.
    
  • OpenJDKの開発メーリングリストから拾った内容

    The stock HotSpot VM (the one in Oracle's Java SE JDK and OpenJDK) uses the
    same stack for Java and native methods for a Java thread; Java frames and
    native frames can be mixed together in such a stack.
    -Xss/-XX:ThreadStackSize controls the whole stack's size for Java threads.
    

    情報源: What the difference between -Xss and -XX:ThreadStackSize is?

3 Javaスレッドのスタックページ構成

HotSpotの実装から見るとJVMから起動されたJavaスレッドのスタックページは次の形で構成さ れると思います。VM内部スレッドやJITコンパイルスレッドのページ構成はまだ別です。

   --+--                         +------------------------+
     |                          /|                        |\
     |                         / |    StackRedPages       | -XX:StackRedPages=1(4Kb)
     |                        /  |                        |/
     |   HotSpot Guard Pages--   +------------------------+
     |                        \  |                        |\
     |                         \ |    StackYellowPages    | -XX:StackYellowPages=2(8Kb)
     |                          \|                        |/
     |                           +------------------------+
     |                          /|                        |\ ★Native Stackはここです★
     |                         / |    StackShadowPages    | -XX:StackShadowPages=20(80Kb)
-XX:ThreadStackSize           /  |                        |/
     |                       /   +------------------------+
     |                      /    |                        |\
     |                     /     |   +----------------+   | \
     |       Normal Stack--      |   |     Frame      |   |  \
     |                     \     |   +----------------+   |   \
     |                      \    |   |     Frame      |   |    ★Java Stackはここです★
     |                       \   |   +----------------+   |   /
     |                        \  |   |     Frame      |   |  /
     |                         \ |   +----------------+   | /
     |                          \|                        |/
   --+--                         +------------------------+

図2

4 HotSpotの実装

以下はHotSpotのソースコードのコメントに書かれたスタックページ構成図です。

jdk7:hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp

// 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()

図3

図の内容によるとJavaスレッドと非Javaスレッドのスタックページ構成が異なる。 以下はJVMの非Javaスレッドのリストです。

スレッド名 説明
VM thread JVM自身のコアスレッド
Periodic task thread WatcherThreadのシングルトンインスタンス、定義的なVMタスクを実行する
GC threads その名の通りです、メモリ管理自動化役を務める
Compiler threads ByteCodeからアセンブラにコンパイルするスレッド
Signal dispatcher thread 外部からシグナルをハンドリングする役を務める

図3の各領域についてソースコードを見ながら解説していきます。

4.1 glibc guard page

glibc guard page はスタックポインタのオーバーフローを防ぐための-ガードページ。Java スレッドには HotSpot Guard Pages が別途用意されているため、この領域のサイズが0であ る。非Javaスレッドはスタック頂上位置に1ページ分のガードページが割り当てられる。以下は その実装内容です。

  • スレッド作成時にglibcの pthread_attr_setguardsize 関数にてガードページを作成してい る

    jdk7/hotspot/src/os/linux/vm/os_linux.cpp#l923

    // Thread start routine for all newly created threads
    static void *java_start(Thread *thread) {
        ..............
        // glibc guard page
        pthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type));
        ..............
    }
    
  • スレッド種別によってガードページのサイズを決める

    jdk7/hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp#l662

    size_t os::Linux::default_guard_size(os::ThreadType thr_type) {
      // Creating guard page is very expensive. Java thread has HotSpot
      // guard page, only enable glibc guard page for non-Java threads.
      return (thr_type == java_thread ? 0 : page_size());
    }
    

glibc guard page の詳細について以下の情報が参考になると思います。

4.2 HotSpot Guard Pages

Javaスレッドスタックオーバーフローを検出するために書き込み不可の HotSpot Guard Pages 領域がスタック領域のトップ位置に設けている。また HotSpot Guard PagesStackYellowPagesStackRedPages から構成されている。

StackYellowPages はスタックオーバーフローの緩衝域として、余分のメモリーを割り当てま す。 スタックポインターが StackRedPages まで行くとStackOverflowErrorが起きる。

以下はページの構成イメージです。

          +------------------------+
         /|                        |\
HotSpot / |    StackRedPages       | - 1ページ (4Kb)
Guard  /  |                        |/
Pages     +------------------------+
       \  |                        |\
        \ |    StackYellowPages    | - 2ページ (8Kb)
         \|                        |/
          +------------------------+
          |                        |
          |      Normal Stack      |
          |                        |
          +------------------------+

図4

Linux/x86_64環境に置いて、 StackYellowPagesStackRedPages の初期値が2と1である。 それぞれの値は -XX:StackYellowPages-XX:StackRedPages パラメータにて変更するこ とが可能です。

下記は HotSpot Guard Pages の割当処理ロジックです。

  • Javaスレッド起動時のガードページ割当位置やサイズの計算処理

    jdk7:openjdk/hotspot/src/share/vm/runtime/thread.cpp

    void JavaThread::create_stack_guard_pages() {
      if (! os::uses_stack_guard_pages() || _stack_guard_state != stack_guard_unused) return;
    
      // ★ ガードページの位置とサイズの計算
      address low_addr = stack_base() - stack_size();
      size_t len = (StackYellowPages + StackRedPages) * os::vm_page_size();
    
      // ★ ガードページ割当処理はプラットフォーム依存のため、別関数をコール
      int allocate = os::allocate_stack_guard_pages();
      // warning("Guarding at " PTR_FORMAT " for len " SIZE_FORMAT "\n", low_addr, len);
    
      if (allocate && !os::create_stack_guard_pages((char *) low_addr, len)) {
        warning("Attempt to allocate stack guard pages failed.");
        return;
      }
    
      if (os::guard_memory((char *) low_addr, len)) {
        _stack_guard_state = stack_guard_enabled;
      } else {
        warning("Attempt to protect stack guard pages failed.");
        if (os::uncommit_memory((char *) low_addr, len)) {
          warning("Attempt to deallocate stack guard pages failed.");
        }
      }
    }
    
  • ガードページ割当処理

    jdk7u60:openjdk/hotspot/src/os/linux/vm/os_linux.cpp

    bool os::pd_create_stack_guard_pages(char* addr, size_t size) {
    
      if (os::Linux::is_initial_thread()) {
        // As we manually grow stack up to bottom inside create_attached_thread(),
        // it's likely that os::Linux::initial_thread_stack_bottom is mapped and
        // we don't need to do anything special.
        // Check it first, before calling heavy function.
        uintptr_t stack_extent = (uintptr_t) os::Linux::initial_thread_stack_bottom();
        unsigned char vec[1];
    
        if (mincore((address)stack_extent, os::vm_page_size(), vec) == -1) {
          // Fallback to slow path on all errors, including EAGAIN
          stack_extent = (uintptr_t) get_stack_commited_bottom(
                                        os::Linux::initial_thread_stack_bottom(),
                                        (size_t)addr - stack_extent);
        }
    
        if (stack_extent < (uintptr_t)addr) {
          ::munmap((void*)stack_extent, (uintptr_t)(addr - stack_extent));
        }
      }
    
      // ★ここから mmapシステムコールが発行される。
      //  最後の引数に書き込み不可のフラグが付与された
      return os::commit_memory(addr, size, !ExecMem);
    }
    

以下は JBoss AS7 アプリケーションサーバ実行時にワーカスレッドのスタック仮想メモリ割 当状況です。

$ cat /proc/`ps -ef | grep [j]boss.modules.system | awk '{print $2}'`/smaps
...省略...
7ff751076000-7ff751079000 ---p 00000000 00:00 0
Size:                 12 kB    ★説明: StackRedPages(4Kb) + StackYellowPages(8Kb) = 12Kb
Rss:                   0 kB
Pss:                   0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            0 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: mr mw me ac          ★説明:書き込み不可
7ff751079000-7ff751177000 rw-p 00000000 00:00 0                          [stack:21275]
Size:               1016 kB   ★説明: ここからNormal Stackページ
Rss:                 108 kB
Pss:                 108 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:       108 kB
Referenced:          108 kB
Anonymous:           108 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd wr mr mw me ac
...省略...

次のSystemTapスクリプトで HotSpot Guard Pages の割当処理をトレースしてみた。

jvm_memory_trace.stp

#!/usr/bin/stap -p4
probe process("/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so").function("commit_memory_impl")
{
   printf("---------------------------------------------------------\n")
   printf("%d\t%s\n", tid(), $$parms)
   print_ustack(ubacktrace())
}

出力結果

  |$ stap jvm_memory_trace.stp -c "java  -version"
  |Using a compile server.
  |WARNING: Missing unwind data for module, rerun with 'stap -d ...dk-1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/jli/libjli.so'
  |WARNING: Missing unwind data for module, rerun with 'stap -d /usr/lib64/libpthread-2.17.so'
  |java version "1.7.0_75"
  |OpenJDK Runtime Environment (rhel-2.5.4.7.el7_1-x86_64 u75-b13)
  |OpenJDK 64-Bit Server VM (build 24.75-b04, mixed mode)
  |
  |★省略★
  |--------------------------------------------------------------------------------------------------------------
①|12179	exec=0x0 size=0x3000 addr=0x7f1d05b57000  ★commit_memory_impl関数実行時の引数情報
  | 0x7f1d04808371 : _ZN2os16pd_commit_memoryEPcmb+0x1/0xf0 [...1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so]
  | 0x7f1d04802dee : _ZN2os13commit_memoryEPcmb+0x2e/0xd0 [...1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so]
②| 0x7f1d048092df : _ZN2os27pd_create_stack_guard_pagesEPcm+0x7f/0x180 [...1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so]
  | 0x7f1d04945519 : _ZN7Threads9create_vmEP14JavaVMInitArgsPb+0x339/0x1550 [...1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so]
  | 0x7f1d0463fca7 : JNI_CreateJavaVM+0x67/0x2a0 [...1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so]
  | 0x7f1d0562aa68 : 0x7f1d0562aa68 [...dk-1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/jli/libjli.so+0x2a68/0x20e000]
  |--------------------------------------------------------------------------------------------------------------
  |★以降省略★

出力結果に①の commit_memory_impl 仮想メモリ割当処理の第2引数にメモリサイズを指定し ています。 size=0x3000 の値が16進数ですので、10進数に変換すると12Kbです。予測通りで すね。

gdbを用いて上記出力結果から②のソースコード位置を特定する方法を以下に示す。

$ gdb /usr/lib/jvm/java-1.7.0-openjdk-1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-64.el7
★一部内容省略★
Reading symbols from /usr/lib/jvm/java-1.7.0-openjdk-1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so...Reading symbols from /usr/lib/debug/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.75-2.5.4.7.el7_1.x86_64/jre/lib/amd64/server/libjvm.so.debug...done.
done.
★出力結果から関数名ぽいの文字列で関数を探す
(gdb) info functions pd_create_stack_guard_pages
All functions matching regular expression "pd_create_stack_guard_pages":

★検索結果
File /usr/src/debug/java-1.7.0-openjdk-1.7.0.75-2.5.4.7.el7_1.x86_64/openjdk/hotspot/src/os/linux/vm/os_linux.cpp:
bool os::pd_create_stack_guard_pages(char*, unsigned long);
(gdb)

4.3 Normal Stack

Javaスレッドを前提に置いて、 Normal Stack には通常Javaメソッド実行時のフレーム情報 が格納される。ただし、スレッドからC/C++メソッドを実行する時も Normal Stack が利用さ れる。スタックのトップ位置にC/C++メソッド実行用の StackShadowPages が設けられている。 Linux/x86_64環境に置いて StackShadowPages の初期値が20である。

              +------------------------+
             /|                        |\
            / |    StackShadowPages    | -XX:StackShadowPages=20(80Kb)
           /  |                        |/ ★Native Stackはここです!
          /   +------------------------+
         /    |                        |\
        /     |   +----------------+   | \
Normal Stack  |   |     Frame      |   |  \
        \     |   +----------------+   |   \
         \    |   |     Frame      |   |    - ★Java Stackはここです!
          \   |   +----------------+   |   /
           \  |   |     Frame      |   |  /
            \ |   +----------------+   | /
             \|                        |/
              +------------------------+

図5

以下は StackShadowPages 初期値の代入処理ロジックです。

jdk7:hotspot/src/cpu/x86/vm/globals_x86.hpp#l60

#ifdef AMD64
// Very large C++ stack frames using solaris-amd64 optimized builds
// due to lack of optimization caused by C++ compiler bugs
define_pd_global(intx, StackShadowPages, NOT_WIN64(20) WIN64_ONLY(6) DEBUG_ONLY(+2));
#else
define_pd_global(intx, StackShadowPages, 6 DEBUG_ONLY(+5));
#endif // AMD64

StackShadowPages に関して以下の情報が参考になると思います。

  • Troubleshooting Guide for HotSpot VM 4.1.3 Crash due to Stack Overflow
  • JDK-7059899 : Stack overflows in Java code cause 64-bit JVMs to exit due to SIGSEGV
  • What does the StackShadowPages JVM setting do?

    StackShadowPages reserves a portion of the thread stack for native layer
    allocations. The page size usually is 4096b, which mean that 20 pages would occupy
    80Kb. The thread stack is sized through -Xss.
    
    Consider some config examples:
    
       -Xss1024k -XX:StackShadowPages=10
    
       [       984kb java stack    | 40kb native stack]
    
       -Xss1024k -XX:StackShadowPages=20
    
       [       944kb java stack    |   80kb native stack]
    
       -Xss512k -XX:StackShadowPages=10
    
       [ 472kb java stack | 40kb native stack]
    
    If you decrease just -Xss, the overall stack is decreased, but the StackShadowPages
    native reservation is not; only the java portion would lose space. Likewise if you
    only increase -Xss, only the java portion gains space with the increased stack.
    
    If you increase StackShadowPages, the java portion becomes smaller so that the native
    portion can be larger. If the native portion of a stack is exhausted, the JVM can
    fatally crash so sometimes StackShadowPages needs to be increased.
    

5 スタックサイズの制御

従いましてJava Methodに使えるスタック領域(Java Stack)のサイズは次の式で計算出来る。

Java Satck Size = Thread::stack_size() - ((StackRedPages + StackYellowPages + StackShadowPages) * PageSize)

Thread::stack_size() の値はスレッド起動時にglibcの pthread_attr_setstacksize 関数を 用いて設定される。以下はHotSpotの実装です。

  • JVM起動時に実行される処理 jdk7/hotspot/src/os/linux/vm/os_linux.cpp#4820

    // this is called _after_ the global arguments have been parsed
    jint os::init_2(void)
    {
      // ★一部省略★
    
    
      // ★スレッドに割当るスタックサイズの最小許容値の計算
    
      // Check minimum allowable stack size for thread creation and to initialize
      // the java system classes, including StackOverflowError - depends on page
      // size.  Add a page for compiler2 recursion in main thread.
      // Add in 2*BytesPerWord times page size to account for VM stack during
      // class initialization depending on 32 or 64 bit VM.
      os::Linux::min_stack_allowed = MAX2(os::Linux::min_stack_allowed,
                (size_t)(StackYellowPages+StackRedPages+StackShadowPages) * Linux::page_size() +
                        (2*BytesPerWord COMPILER2_PRESENT(+1)) * Linux::vm_default_page_size());
    
    #ifdef ZERO
      // If this is Zero, allow at the very minimum one page each for the
      // Zero stack and the native stack.  This won't make any difference
      // for 4k pages, but is significant for large pages.
      os::Linux::min_stack_allowed = MAX2(os::Linux::min_stack_allowed,
                 (size_t)(StackYellowPages+StackRedPages+StackShadowPages+2) * Linux::page_size());
    #endif
    
      size_t threadStackSizeInBytes = ThreadStackSize * K;
      if (threadStackSizeInBytes != 0 &&
          threadStackSizeInBytes < os::Linux::min_stack_allowed) {
            tty->print_cr("\nThe stack size specified is too small, "
                          "Specify at least %dk",
                          os::Linux::min_stack_allowed/ K);
            return JNI_ERR;
      }
    
      // ★-XX:ThreadStackSizeの値を静的_stack_size_at_create変数に代入する
      // Make the stack size a multiple of the page size so that
      // the yellow/red zones can be guarded.
      JavaThread::set_stack_size_at_create(round_to(threadStackSizeInBytes,
            vm_page_size()));
    
      // ★イニシャルスレッドのスタックサイズ設定処理(★TODO: 別途調査する)
      Linux::capture_initial_stack(JavaThread::stack_size_at_create());
    
      // ★以降省略★
    
  • 新規スレッド起動時の処理 jdk7:hotspot/src/os/linux/vm/os_linux.cpp#901

    bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
    
      // ★一部省略★
    
      // ★スレッド種別毎にスタックサイズを決める
      if (os::Linux::supports_variable_stack_size()) {
        // calculate stack size if it's not specified by caller
        if (stack_size == 0) {
          stack_size = os::Linux::default_stack_size(thr_type);
    
          switch (thr_type) {
    
          //★Javaワーカスレッドの場合
          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;
    
          //★JITコンパイラスレッドの場合
          case os::compiler_thread:
            if (CompilerThreadStackSize > 0) {
              stack_size = (size_t)(CompilerThreadStackSize * K);
              break;
            } // else fall through:
              // use VMThreadStackSize if CompilerThreadStackSize is not defined
    
          //★VMスレッド、GCスレッド、ウォッチャースレッドの場合
          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;
          }
        }
    
        stack_size = MAX2(stack_size, os::Linux::min_stack_allowed);
        // ★glic関数用いてstack領域を確保する
        pthread_attr_setstacksize(&attr, stack_size);
    
        // ★以降省略★
    

以上コードの通り、JVMから起動されたスレッドの種別毎のスタックサイズが下記XXパラメータ値が適用されてい る。

引数 Linux/x86_64環境初期値 適用範囲
-XX:ThreadStackSize 1M Javaスレッド
-XX:VMThreadStackSize 1M VM thread、GC threads、VM Periodic Task Threadなど
-XX:CompilerThreadStackSize 4M C1 C2 CompilerThread

以下は実機にて確認された各パラメータの初期値です。

$ java -XX:+PrintFlagsFinal -version | grep -e "CompilerThreadStackSize\|ThreadStackSize\|VMThreadStackSize"
     intx CompilerThreadStackSize                   = 0               {pd product}
     intx ThreadStackSize                           = 1024            {pd product}
     intx VMThreadStackSize                         = 1024            {pd product}
java version "1.7.0_75"
OpenJDK Runtime Environment (rhel-2.5.4.7.el7_1-x86_64 u75-b13)
OpenJDK 64-Bit Server VM (build 24.75-b04, mixed mode)

CompilerThreadStackSize が指定しない場合下記コードにて初期値が代入される。

jdk7:hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp#652

// return default stack size for thr_type
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;
}

次のサンプルプログラムを実行して、各スレッドのスタックサイズ値を実測してみる。

HelloWorld.java

public class HelloWorld implements Runnable {

    public void run(){
        while(true) {
            try {
                Thread.sleep(1 * 1000L);
                System.out.println(Thread.currentThread().toString() + ": Hello World");
            } catch (Exception e) {
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Thread[] t_arry =  new Thread[5];
        for (int i = 0; i < t_arry.length; i++) {
            t_arry[i]  =  new Thread(new HelloWorld());
            t_arry[i].start();
        }
        for (int i = 0; i < t_arry.length; i++) {
            t_arry[i].join();
        }

    }
}

スタックサイズの実測値は次のスクリプトで取得しています。

jvm_stacksize.sh

#!/bin/sh

if [ ! $# -eq 1 ]; then
    echo "Usage ${0} <JVM PID>"
    exit 1
fi

printf "[ PID ]\t[StackSize]\t[GuardPages]\t[Thread Name]\n"

# jstackの出力結果からスレッドIDと名前を抽出する
jstack $1 | grep nid | sed -e "s/^\"\(.*\)\".*nid=\(0x[0-9|a-z]*\).*$/\2,\1/" | sort | while read line
do
    # スレッドIDを切り出す
    pid_hex=`echo "${line}" | awk -F"," '{print $1}'`

    # スレッド名を切り出す
    thread_name=`echo "${line}" | awk -F"," '{print $2}'`

    # スレッドIDを10進数に変換
    pid=`printf '%d\n' ${pid_hex}`

    # /proc/<pid>/smaps ファイルからスタックサイズ、ガードページサイズを取得する
    guard_page=`cat /proc/$1/smaps | grep -B15 "stack:${pid}"| head -1 | awk '{print $2}'`
    stack_page=`cat /proc/$1/smaps | grep -A1 "stack:${pid}" | tail -1 | awk '{print $2}'`
    stack_size=`expr ${guard_page} + ${stack_page}`
    printf "%7d\t%11s\t%12s\t%s\n" "${pid}" "${stack_size}Kb" "${guard_page}Kb" "${thread_name}"
done

スタックサイズを明示的に指定して、サンプルを実行する。

  • -XX:ThreadStackSize=512
  • -XX:VMThreadStackSize: 2048
  • -XX:CompilerThreadStackSize=3072
java -XX:VMThreadStackSize=2048 -XX:CompilerThreadStackSize=3072 -XX:ThreadStackSize=512 HelloWorld
Thread[Thread-0,5,main]: Hello World
Thread[Thread-4,5,main]: Hello World
Thread[Thread-1,5,main]: Hello World
Thread[Thread-3,5,main]: Hello World
Thread[Thread-2,5,main]: Hello World
★以降は省略

測定結果

$ ./jvm_stacksize.sh `ps -ef | grep "[j]ava.*HelloWorld" | awk '{print $2}'`
[ PID ]	[StackSize]	[GuardPages]	[Thread Name]
  17285      1048Kb         12Kb	main
  17286      2096Kb          4Kb	GC task thread#0 (ParallelGC)
  17287      2052Kb          4Kb	GC task thread#1 (ParallelGC)
  17288      2052Kb          4Kb	GC task thread#2 (ParallelGC)
  17289      2052Kb          4Kb	GC task thread#3 (ParallelGC)
  17290      2052Kb          4Kb	VM Thread
  17291     64584Kb         12Kb	Reference Handler
  17292       516Kb         12Kb	Finalizer
  17293       516Kb         12Kb	Signal Dispatcher
  17294      3076Kb         12Kb	C2 CompilerThread0
  17295      3076Kb         12Kb	C2 CompilerThread1
  17296       516Kb         12Kb	Service Thread
  17297      2052Kb          4Kb	VM Periodic Task Thread
  17298       516Kb         12Kb	Thread-0
  17299       516Kb         12Kb	Thread-1
  17300       516Kb         12Kb	Thread-2
  17301       516Kb         12Kb	Thread-3
  17302       516Kb         12Kb	Thread-4
  17342       516Kb         12Kb	Attach Listener

実測値はXXパラメータで指定した値より1ページ分多い。これは glibc 内部の allocate_stack 処理で追加されているものです。

6 ulimit -sの影響範囲

Linux環境に置いて、JVM内のスレッドは全てglibcの pthread_create 関数経由で起動される。 スレッド起動時にスタックサイズ明示的に指定していない場合、 ulimit -s で設定された値 がスタックのデフォルトサイズとして適用される。前文に書いた通りJVMはスレッド起動時に明 示的 pthread_attr_setstacksize 関数でXXパラメータ値の元にスタックサイズを指定してい るため、これらのスレッドのスタックサイズは ulimit -s の値に影響されないだ。

ただし、JVMランチャー自身は ulimit -s の制限値が適用される。

ulimit コマンドでスタックの上限値 RLIMIT_STACK を64Kbを設定し、サンプルプログラム グライムを実行すると、ランチャーのスタックサイズが60Kbで収まった。

$ ulimit -s 64
$ java -Xss1024K -XX:VMThreadStackSize=2048 -XX:CompilerThreadStackSize=3072 -XX:ThreadStackSize=512 HelloWorld
Thread[Thread-1,5,main]: Hello World
Thread[Thread-3,5,main]: Hello World
Thread[Thread-0,5,main]: Hello World
Thread[Thread-2,5,main]: Hello World
Thread[Thread-4,5,main]: Hello World
★省略

pmap コマンドで仮想メモリマップの最上位アドレス近くにランチャーのスタックサイズを確 認することができる。

$ pmap `ps -ef | grep "[j]ava.*HelloWorld" | awk '{print $2}'`
★省略
00007f39d068d000      4K r---- ld-2.17.so
00007f39d068e000      4K rw--- ld-2.17.so
00007f39d068f000      4K rw---   [ anon ]
00007fff7cf79000     60K rw---   [ stack ] ★ランチャーのスタックサイズ
00007fff7cffe000      8K r-x--   [ anon ]
ffffffffff600000      4K r-x--   [ anon ]
 total          3513684K

JVM内の各スレッドのスタックサイズは下記の通りです、 RLIMIT_STACK に影響されていない ことが分かります。

]$ ./jvm_stacksize.sh `ps -ef | grep "[j]ava.*HelloWorld" | awk '{print $2}'`
[ PID ]	[StackSize]	[GuardPages]	[Thread Name]
  10770      1048Kb         12Kb	main
  10771      2096Kb          4Kb	GC task thread#0 (ParallelGC)
  10772      2052Kb          4Kb	GC task thread#1 (ParallelGC)
  10773      2052Kb          4Kb	GC task thread#2 (ParallelGC)
  10774      2052Kb          4Kb	GC task thread#3 (ParallelGC)
  10775     15812Kb          4Kb	VM Thread
  10776       516Kb         12Kb	Reference Handler
  10777     63556Kb         12Kb	Finalizer
  10778       516Kb         12Kb	Signal Dispatcher
  10779      3076Kb         12Kb	C2 CompilerThread0
  10780      3076Kb         12Kb	C2 CompilerThread1
  10781       516Kb         12Kb	Service Thread
  10782      2052Kb          4Kb	VM Periodic Task Thread
  10783       516Kb         12Kb	Thread-0
  10784       516Kb         12Kb	Thread-1
  10785       516Kb         12Kb	Thread-2
  10786       516Kb         12Kb	Thread-3
  10787       516Kb         12Kb	Thread-4
  11203       516Kb         12Kb	Attach Listener

7 -Xssと-XX:ThreadStackSizeの違い

-Xss-XX:ThreadStackSize 両方ともJavaスレッドのスタックを指定するパラメータで ある。 ただし、JVMランチャーから起動されたイニシャルスレッドのスタックサイズの制御は -Xss パラメータのみできる。

以下はJVMランチャーからイニシャルスレッド起動するまでの流れ

行
 | ★ランチャーの実行
1| openjdk/jdk/src/share/bin/main.c:93           ==> int main(int, char **);
2| openjdk/jdk/src/share/bin/java.c:170            ==> int JLI_Launch(int, char **, int, const char **, int, const char **, const char *, const char *, const char *, const char *, jboolean, jboolean, jboolean, jint);
3| openjdk/jdk/src/share/bin/java.c:1835             ==> int ContinueInNewThread(InvocationFunctions *, jlong, int, char **, int, char *, int);
 | ★イニシャルスレッド起動
4| openjdk/jdk/src/solaris/bin/java_md_solinux.c:1021  ==> int ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args)
5| openjdk/jdk/src/share/bin/java.c:337                  ==> int JavaMain(void *);
 | openjdk/jdk/src/share/bin/java.c:1097                 ==> jboolean InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
6| openjdk/hotspot/src/share/vm/prims/jni.cpp            ==> jint JNI_CreateJavaVM(JavaVM**, void**, void*);
7| openjdk/hotspot/src/share/vm/runtime/thread.cpp:3271    ==> jint Threads::create_vm(JavaVMInitArgs*, bool*);
8| openjdk/hotspot/src/os/linux/vm/os_linux.cpp:4898         ==> jint os::init_2(void)
9| openjdk/hotspot/src/os/linux/vm/os_linux.cpp:1205           ==> void os::Linux::capture_initial_stack(size_t max_size)
  • 行1: JVMランチャーのmain関数
  • 行2: JLI_Launch 関数にてコマンドラインパラメータのパーシング処理が実行される。
  • 行3: -Xss パラメータが指定されていない場合、デフォルト値(1024Kb)を取得し(4)に渡す。
  • 行4: glibcの pthread_create 関数を用いてイニシャルスレッドを起動する。 -Xss の 値がスタックサイズに適用される。

-Xss-XX:ThreadStackSize 片方指定する場合、と両方指定する場合効果が違うので要注意です。

  イニシャルスレッド ワーカスレッド
-Xss2048K 2048K 2048K
-XX:ThreadStackSize=2048 1024K 2048K
-Xss2048K 2048K 512K
-XX:ThreadStackSize=512    

Comments