第6章 HotspotVMのスレッド管理

この章から数章をかけてHotspotVMのスレッド管理方法について見ていきます。『アルゴリズム編』で述べたとおり、G1GCは並列・並行GCを組み合わせたGCです。並列・並行GCはそれぞれスレッドを利用して実装されており、複数のスレッドをどのように管理するかが実装の肝になります。

本章ではHotspotVMのスレッド管理の中でもOSに近い土台的な部分を見ていきます。

6.1 スレッド操作の抽象化

WindowsやLinuxにはそれぞれOSのスレッドを利用するライブラリが存在します。WindowsではWindows APIを使ってスレッドを操作し、LinuxではPOSIXスレッド標準を実装したライブラリであるPthreadsを利用します。

HotspotVM内には、OSよって異なるスレッド操作を共通化する層を設けており、HotspotVM内でスレッドが簡単に利用できるように工夫されています。

6.2 Threadクラス

HotspotVM内ではスレッドを操作する基本的な機能をThreadクラスによって実装し、Threadクラスを継承した子クラスの実装によってスレッドの振る舞いを決定します。図6.1にThreadクラスの継承関係を示します。

Threadクラスの継承関係

図6.1: Threadクラスの継承関係

ThreadクラスはCHeapObjクラスを継承しているため、インスタンスはCのヒープ領域から直接アロケーションされます。

Threadクラスは仮想関数としてrun()が定義されています。

share/vm/runtime/thread.hpp

94: class Thread: public ThreadShadow {
    ...
1428:  public:
1429:   virtual void run();

run()は生成したスレッド上で実行される関数です。Threadクラスを継承した子クラスでrun()を実装し、実際にスレッド上で動作させる処理を定義します。

親クラスのThreadShadowクラスはスレッド実行中に発生した例外を統一的に扱うためのクラスです。

子クラスのJavaThreadクラスはJavaの言語レベルで実行されるスレッドを表現しています。言語利用者がJavaのスレッドを一つ作ると、内部ではこのJavaThreadクラスが一つ生成されています。JavaThreadクラスはGCとそれほど関係のないクラスですので、本書では詳しく説明しません。

NamedThreadクラスはスレッドに対する名前付けをサポートします。NamedThreadクラスやその子クラスでは、インスタンスに対して一意の名前を設定できます。GCスレッドとして利用するクラスは、このNamedThreadクラスを継承して実装されます。

6.3 スレッドのライフサイクル

では、実際にスレッドが生成され、処理の開始・終了までを順を追ってみていきましょう。以下がひとつのスレッドのライフサイクルです。

  1. Threadクラスのインスタンス生成
  2. スレッド生成(os::create_thread()
  3. スレッド処理開始(os::start_thread()
  4. スレッド処理終了
  5. Threadクラスのインスタンス解放

まず、1.でThreadクラスのインスタンスを生成します。インスタンス生成時にスレッドを管理するためのリソースを初期化したり、スレッドを生成する前準備を行います。

2.,3.,4.については図6.2に図示しました。


図6.2: スレッド生成・処理開始・処理終了の流れ図

2.で実際にスレッドを生成します。この段階ではスレッドを一時停止した状態で作っておきます。

3.で停止していたスレッドを起動します。この段階で、Threadクラスの子クラスで実装されたrun()メンバ関数が、生成したスレッド上で呼び出されます。

4.のように、run()メンバ関数の処理が終わると、スレッドの処理は終了します。

5.でThreadクラスのインスタンスを解放ます。その際にデストラクタにてスレッドで利用してきたリソースも合わせて解放されます。

OSThreadクラス

ThreadクラスにはOSThreadクラスのインスタンスを格納する_osthreadメンバ変数が定義されています。OSThreadクラスはスレッドを操作するのに必要な、各OSに依存したスレッドの情報を保持します。スレッド生成時にOSThreadクラスのインスタンスが生成され、_osthreadメンバ変数に格納されます。

OSThreadクラスの定義では対象のOSごとに異なる下記のようにヘッダファイルを読み込みます。

share/vm/runtime/osThread.hpp

61: class OSThread: public CHeapObj {
   ...
67:   volatile ThreadState _state; // スレッドの状態
   ...
102:   // Platform dependent stuff
103: #ifdef TARGET_OS_FAMILY_linux
104: # include "osThread_linux.hpp"
105: #endif
106: #ifdef TARGET_OS_FAMILY_solaris
107: # include "osThread_solaris.hpp"
108: #endif
109: #ifdef TARGET_OS_FAMILY_windows
110: # include "osThread_windows.hpp"
111: #endif

では、Linuxのヘッダファイルを一部見てみましょう。

os/linux/vm/osThread_linux.hpp

49:   pthread_t _pthread_id;

pthread_tはPthreadsで利用されるデータ型です。_pthread_idには、Pthreadsによるスレッド操作に必要なpthreadのIDが格納されます。

また、OSThreadクラスにはスレッドの現在の状態を保持する_stateメンバ変数が定義されています。_stateメンバ変数の値はThreadStateで定義された識別子が格納されます。

share/vm/runtime/osThread.hpp

44: enum ThreadState {
45:   ALLOCATED,    // アロケーション済みだが初期化はまだ
46:   INITIALIZED,  // 初期化済みだが処理開始はまだ
47:   RUNNABLE,     // 処理開始済みで起動可能
48:   MONITOR_WAIT, // モニターロック競合待ち
49:   CONDVAR_WAIT, // 条件変数待ち
50:   OBJECT_WAIT,  // Object.wait()呼び出しの待ち
51:   BREAKPOINTED, // ブレークポイントで中断
52:   SLEEPING,     // Thread.sleep()中
53:   ZOMBIE        // 終了済みだが回収されていない
54: };

_stateメンバ変数は各OS共通で定義され、ThreadStateの識別子も各OSで同じものが利用されます。

6.4 Windowsのスレッド生成

この節からは各OSでどのようにスレッドを扱っているのかを見ていきます。最初はWindows環境でのスレッド生成です。

スレッドを生成するメンバ関数はos::create_thread()に定義されています。os::create_thread()内でおこなう処理の概要を次に示しました。

  1. OSThreadのインスタンスを生成
  2. スレッドで使用するマシンスタックのサイズを決定
  3. スレッドを生成し、スレッドの情報を格納
  4. スレッドの状態をINITIALIZEDに変更

では、実際にos::create_thread()の中身を見ていきましょう。

os/windows/vm/os_windows.cpp:os::create_thread()

510: bool os::create_thread(Thread* thread,
                            ThreadType thr_type,
                            size_t stack_size) {
511:   unsigned thread_id;
512:
513:   // 1.OSThreadのインスタンスを生成
514:   OSThread* osthread = new OSThread(NULL, NULL);
528:   thread->set_osthread(osthread);

はじめにOSThreadのインスタンスを生成して、引数にとったThreadのインスタンスに格納します。

os/windows/vm/os_windows.cpp:os::create_thread()

       // 2.スレッドで使用するスタックサイズの決定
530:   if (stack_size == 0) {
531:     switch (thr_type) {
532:     case os::java_thread:
533:       // -Xssフラグで変更可能
534:       if (JavaThread::stack_size_at_create() > 0)
535:         stack_size = JavaThread::stack_size_at_create();
536:       break;
537:     case os::compiler_thread:
538:       if (CompilerThreadStackSize > 0) {
539:         stack_size = (size_t)(CompilerThreadStackSize * K);
540:         break;
541:       }
           // CompilerThreadStackSizeが0ならVMThreadStackSizeを設定する
543:     case os::vm_thread:
544:     case os::pgc_thread:
545:     case os::cgc_thread:
546:     case os::watcher_thread:
547:       if (VMThreadStackSize > 0)
             stack_size = (size_t)(VMThreadStackSize * K);
548:       break;
549:     }
550:   }

その後、スレッドで使用するマシンスタックのサイズを決定します。os::create_thread()の引数にとった、スタックサイズ(stack_size)が0であれば、スレッドの種類(thr_type)に対する適切なサイズを決定します。利用する範囲のスタックサイズを指定し、無駄なメモリ消費を抑えるのがこの処理の狙いです。

CompilerThreadStackSizeVMThreadStackSizeはOS環境によって決定されますが、JavaThread::stack_size_at_create()についてはJavaの起動オプションによって言語利用者が指定可能です。

os/windows/vm/os_windows.cpp:os::create_thread()

     3. スレッドを生成し、スレッドの情報を格納
573: #ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
574: #define STACK_SIZE_PARAM_IS_A_RESERVATION  (0x10000)
575: #endif
576:
577:   HANDLE thread_handle =
578:     (HANDLE)_beginthreadex(NULL,
579:       (unsigned)stack_size,
580:       (unsigned (__stdcall *)(void*)) java_start,
581:       thread,
582:       CREATE_SUSPENDED | STACK_SIZE_PARAM_IS_A_RESERVATION,
583:       &thread_id);

606:   osthread->set_thread_handle(thread_handle);
607:   osthread->set_thread_id(thread_id);

次にWindows APIである_beginthreadex()関数を使ってスレッドを生成します。_beginthreadex()関数の引数には次の情報を渡します。

  1. スレッドのセキュリティ属性。NULLの場合は何も指定されません。
  2. スタックサイズ。0の場合はメインスレッドと同じ値を使用します。
  3. スレッド上で処理する関数のアドレス。
  4. 3.に指定した関数に渡す引数。
  5. スレッドの初期状態。CREATE_SUSPENDEDは一時停止を表します。
  6. スレッドIDを受け取る変数へのポインタ。

上記の他に5.(スレッドの初期状態)に対してSTACK_SIZE_PARAM_IS_A_RESERVATIONフラグを指定していますが、このフラグの詳細はすぐ後の項で説明します。

606、607行目ではスレッド生成時に取得したthread_handlethread_idOSThreadインスタンスに設定します。

os/windows/vm/os_windows.cpp:os::create_thread()

       4. スレッドの状態をINITIALIZEDに変更
610:   osthread->set_state(INITIALIZED);

613:   return true;
614: }

最後にスレッドの状態をINITIALIZEDに変更して、os::create_thread()の処理は終了です。

STACK_SIZE_PARAM_IS_A_RESERVATIONフラグ

ソースコード中のコメントによれば、_beginthreadex()stack_sizeの指定にはかなりクセがあり、それを抑止するためにSTACK_SIZE_PARAM_IS_A_RESERVATIONフラグが指定されているようです。ソースコード(os/windows/vm/os_windows.cpp)内のコメントを簡単に以下に翻訳しました。

MSDNのドキュメントに書いてあることと実際の動作は違い、_beginthreadex()の"stack_size"はスレッドのスタックサイズを定義しません。その代わりに、最初のコミット済みメモリサイズを定義します。スタックサイズは実行ファイルのPEヘッダ(*1)によって定義されます。ランチャーのスタックサイズのデフォルト値が320kBだったとしましょう。その場合、320kB以下の"stack_size"はスレッドのスタックサイズにたいして何の影響も与えません。"stack_size"がPEヘッダのデフォルト値(この場合320kB)より大きい場合、スタックサイズは最も近い1MBの倍数に切り上げられます。そしてこれは最初のコミット済みメモリサイズに影響があります。つまり、"stack_size"がPFヘッダのデフォルト値より大きい場合、重大なメモリ使用量の増加を引き起こす可能性があるのです。なぜなら、意図せずにスタック領域が数MBに切り上げられるだけでなく、その全体の領域が前もって確保されてしまうからです。

最終的にWindows XPはCreateThread()のために"STACK_SIZE_PARAM_IS_A_RESERVATION"を追加しました。これは"stack_size"を「スタックサイズ」として扱えるようになるフラグです。ただ、JVMはCランタイムライブラリを利用するため、MSDNに従うとCreateThread()を直接呼ぶことができません(*2)。

でも、いいニュースです。このフラグは_beginthreadex()でもうまく動くようですよ!!

*1:訳注 実行ファイルに定義される、実行に必要な設定を格納する場所のことをPEヘッダと呼ぶ。

*2:訳注 そのため、_beginthreadex()を利用している。

Windows APIの暗黒面を垣間見ましたが、STACK_SIZE_PARAM_IS_A_RESERVATION_beginthreadex()の引数に指定している理由はわかりました。

6.5 Windowsのスレッド処理開始

親スレッドは、os::create_thread()処理後にos::start_thread()を呼び出して生成したスレッドの処理を開始します。このメンバ関数は各OSで共通のものです。

share/vm/runtime/os.cpp

695: void os::start_thread(Thread* thread) {

698:   OSThread* osthread = thread->osthread();
699:   osthread->set_state(RUNNABLE);
700:   pd_start_thread(thread);
701: }

699行目でスレッドの状態をRUNNABLEに設定し、700行目でos::pd_start_thread()を呼び出します。os::pd_start_thread()は各OSで異なるものが定義されています。

os/windows/vm/os_windows.cpp

2975: void os::pd_start_thread(Thread* thread) {
2976:   DWORD ret = ResumeThread(thread->osthread()->thread_handle());

2982: }

2976行目でWindows APIのResumeThread()関数を呼び出し、一時中断していたスレッドを実行します。スレッド上ではじめに起動する関数は、_beginthreadex()の引数に渡していたjava_start()です。

os/windows/vm/os_windows.cpp

391: static unsigned __stdcall java_start(Thread* thread) {

421:        thread->run();

435:   return 0;
436: }

421行目で、Threadクラスを継承した子クラスで定義した、run()が実行されます。

キャッシュラインの有効利用

前節では省略しましたが、java_start()の最初の方に一見意味不明なコードが登場します。

os/windows/vm/os_windows.cpp

391: static unsigned __stdcall java_start(Thread* thread) {
397:   static int counter = 0;
398:   int pid = os::current_process_id();
399:   _alloca(((pid ^ counter++) & 7) * 128);

421:        thread->run();

435:   return 0;
436: }

397〜399行目の処理を簡単に説明します。_alloca()はマシンスタック領域からメモリをアロケーションする関数です。_alloca()に渡される数値は、java_start()の呼び出しごとに、[0..7]の範囲で1ずつずらした値に、128を掛けたものです。プロセスIDは[0..7]の中でずらしはじめる起点を決定します。もっと簡単に言えば、128の間隔でプロセス・スレッドごとに(重複を含む)ずれた値が_alloca()に渡されます。

この処理はマシンスタックが利用するCPUキャッシュラインの利用箇所を分散する役割があります。

CPUのキャッシュラインとはキャッシュメモリに格納するデータ1単位のことを指します。図6.3のようにほとんどのキャッシュメモリは数バイトのキャッシュラインで分割されています。CPUは頻繁に使うデータをこのキャッシュライン単位で格納しています。

現在、多くのCPUにはコアに近い側からL1(レベル1)、L2のキャッシュメモリがある。キャッシュメモリはキャッシュラインという単位でデータが格納される。

図6.3: 現在、多くのCPUにはコアに近い側からL1(レベル1)、L2のキャッシュメモリがある。キャッシュメモリはキャッシュラインという単位でデータが格納される。

問題の処理で懸念しているのは「同じスタックトレースを作るようなスレッドが複数作られた場合、キャッシュラインの利用箇所が偏るかもしれない」ということです。スタックトレースが同じであれば、マシンスタックの各フレームのアドレスの間隔が一致してしまい、スタックフレームが格納されるキャッシュラインが偏る可能性があります。

また、デュアルコアやクアッドコアのCPUではほとんどがL2のキャッシュメモリを共有します。そのため上記の状態に陥ると、スタックにアクセスする際にキャッシュの奪いあいがスレッド間で発生してしまい、スレッドの速度低下につながります。CPUの1プロセッサを2プロセッサに見せかけるハイパースレッディング技術ではL1とL2のキャッシュメモリを共有するため、速度低下はより深刻になります。

そのため、397〜399行目の処理によって、ある程度ずらしたアドレスからマシンスタックをスレッドに作らせることで、キャッシュラインの利用箇所が偏らないようにしています。

6.6 Linuxのスレッド生成

次にLinux環境でのスレッド生成を見てみましょう。「6.4 Windowsのスレッド生成」にて紹介した内容と重複するものは省略します。

os/linux/vm/os_linux.cpp:os::create_thread()

866: bool os::create_thread(Thread* thread,
                            ThreadType thr_type,
                            size_t stack_size) {

870:   OSThread* osthread = new OSThread(NULL, NULL);

876:   osthread->set_thread_type(thr_type);
877:
       // 最初の状態はALLOCATED
879:   osthread->set_state(ALLOCATED);
880:
881:   thread->set_osthread(osthread);
882:
       // スレッドの属性初期化
884:   pthread_attr_t attr;
885:   pthread_attr_init(&attr);
886:   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

       // 省略: スレッドで使用するスタックサイズの決定

923:   pthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type));
924:

870〜881行目でosthreadを初期化します。Windows版と異なるのは、set_state()ALLOCATEDを設定している点です。

884、885行目でスレッドの属性を初期化します。pthread_attr_tはpthreadの属性を格納する構造体です。885行目のpthread_attr_init()関数でattr変数をPthreadsが定めるデフォルト値に初期化します。

886行目のpthread_attr_setdetachstate()でスレッド属性のデタッチ状態を設定します。PTHREAD_CREATE_DETACHEDフラグを設定すると、スレッドはデタッチ状態で生成されます。デタッチ状態のスレッドは、スレッドの処理が終了したときに自動でスレッド自身のリソースが解放されます。ただし、デタッチ状態のスレッドはメインスレッドから切り離された状態で処理を実行しますので、デタッチ状態のスレッドとは合流(join)できません。

923行目ではpthread_attr_setguardsize()でスタックのガード領域サイズを指定しています。この点については後の「6.6.1 スタックのガード領域」で詳しく取り上げます。

os/linux/vm/os_linux.cpp:os::create_thread()

925:   ThreadState state;
926:
927:   {

934:     pthread_t tid;
935:     int ret = pthread_create(&tid, &attr,
                                  (void* (*)(void*)) java_start,
                                  thread);
936:
937:     pthread_attr_destroy(&attr);

         // pthread 情報を OSThread に格納
951:     osthread->set_pthread_id(tid);
952:
         // 子スレッドが初期化 or 異常終了するまで待つ
954:     {
955:       Monitor* sync_with_child = osthread->startThread_lock();
956:       MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);
957:       while ((state = osthread->get_state()) == ALLOCATED) {
958:         sync_with_child->wait(Mutex::_no_safepoint_check_flag);
959:       }
960:     }

965:   }

977:   return true;
978: }

935行目のpthread_create()を使ってスレッドを生成します。引数には次の情報を渡します。

  1. スレッドIDを受け取る変数へのポインタ。
  2. スタックの属性。今まで育ててきたattrへのポインタを指定する。
  3. スレッド上で処理する関数のアドレス。
  4. 3.に指定した関数に渡す引数。

954〜960行目で、作成したスレッドが初期化されるのを待ちます。957行目のwhileループで、スレッドの状態がALLOCATED以外に書き換えられるのを待っています。スレッドの状態はjava_start()の中で書き換えられます。つまり、作成したスレッドの準備が整い、スレッドの処理が実際に実行された時にwhileループを抜けます。958行目のwait()はスレッドを待たせる処理です。wait()の詳細についてはまた後述します。

スタックのガード領域

os::create_thread()では次のように、スタックのガード領域のサイズが指定されます。

os/linux/vm/os_linux.cpp:os::create_thread():再掲

923:   pthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type));

スタックのガード領域とは、スタックのオーバーフローを防ぐ(ガードする)ための緩衝域として用意されるものです。図6.4のように、Linux環境ではマシンスタックの上限のすぐ上に余分にガード領域を設けおり、もしスタックが溢れてガード領域にアクセスした場合にはSEGVのシグナルが発生します。

スタックには、マシンスタックとして使える領域の底の直後にガード領域があり、OSのガード領域にメモリアクセスがあるとスタックオーバーフローとなる。

図6.4: スタックには、マシンスタックとして使える領域の底の直後にガード領域があり、OSのガード領域にメモリアクセスがあるとスタックオーバーフローとなる。

ガード領域のサイズとしてos::Linux::default_guard_size()が指定されています。この関数の定義は次のとおりです。

os_cpu/linux_x86/vm/os_linux_x86.cpp

662: size_t os::Linux::default_guard_size(os::ThreadType thr_type) {
665:   return (thr_type == java_thread ? 0 : page_size());
666: }

Javaスレッド以外は1ページ分のサイズ、Javaスレッドの場合は0のサイズが返されます。つまり、Javaスレッドの場合はガード領域が作成されません。これはなぜでしょうか?

図6.5に示す通り、実はJavaスレッドは独自のガード領域を別に準備しており、スタックオーバーフローエラーのハンドリングや事後処理などを自前で実装しています。そのため、JavaスレッドではOSが用意するガード領域に意味がなく、確保されるメモリ領域がもったいないため、pthread_attr_setguardsize()にて0が指定されます。

Javaスレッドはマシンスタックとして使える領域の底の直前を独自のガード領域として利用している。

図6.5: Javaスレッドはマシンスタックとして使える領域の底の直前を独自のガード領域として利用している。

6.7 Linuxのスレッド処理開始

Linux環境では一時停止状態でスレッド生成できないため、java_start()がすぐに実行されてしまいます。

os/linux/vm/os_linux.cpp

807: static void *java_start(Thread *thread) {

        /* キャッシュラインをずらす処理 */

819:   OSThread* osthread = thread->osthread();
820:   Monitor* sync = osthread->startThread_lock();

       // 親スレッドとのハンドシェイク
847:   {
848:     MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);
849:
         // 待っている親スレッドを起こす
851:     osthread->set_state(INITIALIZED);
852:     sync->notify_all();

         // os::start_thread() の呼び出しを待つ
855:     while (osthread->get_state() == INITIALIZED) {
856:       sync->wait(Mutex::_no_safepoint_check_flag);
857:     }
858:   }

861:   thread->run();
862:
863:   return 0;
864: }

848〜852行目で、待ち状態にある親スレッドに対して、初期化が終わったことを通知します。子スレッドはその後すぐに、855、856行目でos::start_thread()の呼び出し待ちに入ります。

親スレッドは851行目でスレッドの状態が変わったことを検知し、os::create_thread()を終了させ、os::start_thread()を呼び出します。os::start_thread()はWindowsと共通の関数でした。もう一度見てみましょう。

share/vm/runtime/os.cpp:再掲

695: void os::start_thread(Thread* thread) {

698:   OSThread* osthread = thread->osthread();
699:   osthread->set_state(RUNNABLE);
700:   pd_start_thread(thread);
701: }

699行目でスレッドの状態をRUNNABLEに変更しています。これで子スレッドはwhileループを抜けられます。待ちの状態の子スレッドを起こしているのが、pd_start_thread()の処理です。

os/linux/vm/os_linux.cpp

1047: void os::pd_start_thread(Thread* thread) {
1048:   OSThread * osthread = thread->osthread();

1050:   Monitor* sync_with_child = osthread->startThread_lock();
1051:   MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);
1052:   sync_with_child->notify();
1053: }

1052行目で子スレッドを起こします。子スレッドは待ちを抜けた後、run()を呼び出し、ユーザが定義したスレッドの処理を開始します。


御意見・御感想・誤植の指摘などは@nari3もしくはauthorNari/g1gc-impl-book - GitHubまでお願いします。

Webサイトのトップページ

(C) 2011-2012 Narihiro Nakamura