第9章 GCスレッド(並行編)

本章では並行GCに利用するスレッドの概要を説明し、ミューテータと並行で走る並行GCスレッドがどのように制御されているかを見ていきます。

9.1 ConcurrentGCThreadクラス

並列GCはCuncurrentGCThreadクラスを継承したクラスで実装されます。CuncurrentGCThreadクラスの継承関係を図9.1に示します。

<tt class="inline-code">CuncurrentGCThread</tt>クラスの継承関係

図9.1: CuncurrentGCThreadクラスの継承関係

並列GCはミューテータとは別スレッドで動くGCのことを指していますので、もちろんThreadクラスを祖先に持っています。

CuncurrentGCThreadに定義されているcreate_and_start()はスレッドの生成・起動を一度におこなうメンバ関数です。CuncurrentGCThreadを継承するすべての子クラスは、次のようにコンストラクタでcreate_and_start()を呼び出すため、インスタンスを作ったタイミングでGCスレッドが起動します。

share/vm/gc_implementation/g1/concurrentMarkThread.cpp

41: ConcurrentMarkThread::ConcurrentMarkThread(ConcurrentMark* cm) :
42:   ConcurrentGCThread(),
43:   _cm(cm),
44:   _started(false),
45:   _in_progress(false),
46:   _vtime_accum(0.0),
47:   _vtime_mark_accum(0.0),
48:   _vtime_count_accum(0.0)
49: {
50:   create_and_start();
51: }

また、スレッドの処理を定義するrun()はそれぞれの子クラスで定義されます。

9.2 SuspendableThreadSetクラス

並行GCスレッド群はSuspendableThreadSetクラスによって停止・起動の制御をおこないます。SuspendableThreadSetクラスとは、名前のとおり、停止可能なスレッドの集合を管理するクラスです。

CuncurrentGCThreadクラスはSuspendableThreadSetのインスタンスをクラスの静的メンバ変数として保持します。

share/vm/gc_implementation/shared/concurrentGCThread.hpp

78: class ConcurrentGCThread: public NamedThread {

       // すべてのインスタンスで共有
101:   static SuspendibleThreadSet _sts;

101行目で定義された_stsConcurrentGCThreadを継承したすべてのクラスのインスタンスで共有されます。

集合の操作

SuspendableThreadSetクラスをよく知るために、主要なメンバ関数を説明していきましょう。

まず、集合に対して加入・脱退できるメンバ関数が定義されています。

SuspendableThreadSetは生成した段階で集合内に1つもスレッドを持っていません。それぞれのスレッドは加入したい時にjoin()し、脱退したい時にleave()します。

SuspendableThreadSetクラスには、集合内の全スレッドに対して停止・再起動するように要求するメンバ関数も定義されています。

suspend_all()を呼び出したスレッドは集合内の全スレッドが停止するまで待ち状態になります。また、もし停止要求中にjoin()しようとするスレッドがいた場合、そのスレッドも待ち状態になります。

その後、resume_all()を呼び出すと集合内の全スレッドは再起動し、join()待ちのスレッドは待ち状態が解けて集合内に追加されます。

つまり、suspend_all()を呼び出しが完了した後から、resume_all()を呼び出すまで、SuspendableThreadSet内の全スレッドは停止状態にあり、集合にあらたなスレッドが追加されることもありません。

停止するタイミング

suspend_all()を呼び出したあと、集合内のスレッドがすぐに停止するわけではありません。それぞれのスレッドはそれぞれに都合のよいタイミングで停止します。

SuspendableThreadSetには集合内の各スレッドが停止するための次のメンバ関数が定義されています。

各スレッドは、自分が受け持つ処理の節目など、停止してもよいタイミングでyield()を定期的に呼び出すように義務付けられています。

集合外からのyield()呼び出し

実は集合外のスレッドからもyield()を呼ぶことが可能です(集合とはなんだったのか…)。

集合外のスレッドからyield()を呼び出した場合の振る舞いは通常ものと同じです。全停止要求中であれば自スレッドを停止し、resume_all()後に再起動します。

利用イメージ

ここまでに説明した関数の利用例を図9.2に示します。

<tt class="inline-code">SuspendableThreadSet</tt>を利用したスレッドの動作制御例。青い矢印上の処理は<tt class="inline-code">suspend_all()</tt>が成功し、スレッドA・Bが動いていない状態で実行できている。一方、集合外のスレッドCは<tt class="inline-code">suspend_all()</tt>完了後の<tt class="inline-code">yield()</tt>呼び出しで停止する。

図9.2: SuspendableThreadSetを利用したスレッドの動作制御例。青い矢印上の処理はsuspend_all()が成功し、スレッドA・Bが動いていない状態で実行できている。一方、集合外のスレッドCはsuspend_all()完了後のyield()呼び出しで停止する。

まず、メインスレッドはsuspend_all()を呼び出し、集合に停止要求を出します。その後、集合内のすべてのスレッドが停止終わった後で、処理を実行し、最終的にresume_all()を呼び出します。

スレッドAはsuspend_all()呼び出し前にjoin()を呼び出している唯一のスレッドです。そのため、suspend_all()呼び出し時に集合内のスレッドはAのみとなります。スレッドAは定期的にyield()を呼び出しており、suspend_all()後のyield()で自スレッドを停止します。

スレッドBはsuspend_all()呼び出し後にjoin()を呼び出しています。集合は停止要求を受けていますので、join()を呼び出したスレッドBは停止します。

一方、スレッドCは集合とは関係ないスレッドにも関わらず、定期的にyield()を呼び出しています。そして、suspend_all()呼び出し後のyield()で自スレッドを停止します。スレッドCは一時的ではありますが、メインスレッドの青い矢印と同時に動く点に注意してください。集合と関係のないスレッドが停止することはsuspend_all()では保証していません。

まとめると、SuspendibleThreadSetは、集合に関わるスレッドを停止させた状態で何らかの処理を実行できる「仕組み」を提供しています。図9.2をみると、集合に関わるスレッドA・Bが動いていない状態でメインスレッドの青い部分の処理が動くことがわかると思います。そして、各スレッドの停止位置はjoin()yield()の呼び出しタイミングによって任意に決めることができます。そのため、集合内の各スレッドは自身の処理の区切りなどの安全な位置で停止することが可能です。

9.3 セーフポイント

Hotspotにはセーフポイントと呼ばれる謎な用語があります。よく「システム全体の『安全な状態』を『セーフポイント』と呼ぶ」などと説明されますが、正直この説明では安全な状態が具体的に何であるか理解できません。実は、セーフポイントはGCのルートとかなり密接な関係にあり、GCをよく知っていないと説明が難しい用語なのです。そのため、上記の奥歯にものが挟まったような説明になりがちです。

セーフポイントとは?

セーフポイントとは、プロブラム実行中のすべてのルートを矛盾なく列挙できる状態のことを指します。ルートとはマーキングやコピーなどでオブジェクトのポインタをたどる際の起点となる部分のことです。そのため、ルートの「矛盾のない列挙」・「すべて列挙」という2点を満たせなければ、生存オブジェクトを見逃すおそれがあります。

ルートを矛盾なく列挙するのにもっとも簡単は方法は、列挙している間はルートを変更を禁止することです。これについては、ミューテータなどのルートを変更するスレッドを停止する方法が最も簡単です。そのため、HotspotVMではセーフポイントとしてすべてのJavaスレッドを停止します。

じゃあ単純に止めるだけか、というとそうでもありません。スレッドを停止する前に、自分の抱えるルートをGCに見える位置に提供しなければならないのです。そうしないと、GCはすべてのルートを見つけることができません。

具体的な問題としてJITコンパイラの例があります。JITコンパイラでは、メソッドをコンパイルする際に、スタックやレジスタのどの部分がオブジェクトへの参照であるかを示すスタックマップと呼ばれるもの生成します。そして、GCはこのスタックマップを参考にルートを列挙するわけです。生成したスタックマップの保持には容量的なコストがかかるので、特定のタイミングのスタックマップしか生成しません。そのため、セーフポイントとしてスレッドを停止するタイミングは、スタックマップを保持しているタイミングでなければなりません。スタックマップの詳細については、「13.1.9 コンパイル済みフレーム」でまた詳しく説明します。

つまり、セーフポイントとはわかりやすく言ってしまえば、ミューテータのすべてのスレッドを安全に停止している状態です。そして、ここでいう「安全に停止している状態」という意味は、「ルートを安全に列挙できる状態」という意味になります。

並行GCスレッドのセーフポイント

Javaスレッドだけがルートを持っているわけではありません。例えば『アルゴリズム編 3.8 ステップ2 ールート退避』では「並行マーキングで使用中のオブジェクト」をルートとしてあげています。また、退避用記憶集合維持スレッドもミューテータと並行に走っているスレッドであり、退避用記憶集合もルートとして扱われます。つまり、これらの並行GCスレッドでも、きちんとGCに見えるところにルートを提供してから停止する必要があるわけです。

ここで登場するのが「9.2 SuspendableThreadSetクラス」で説明した内容です。セーフポイントを開始するSafepointSynchronize::begin()の一部を見てみましょう。

share/vm/runtime/safepoint.cpp

101: void SafepointSynchronize::begin() {

117:     ConcurrentGCThread::safepoint_synchronize();

117行目でConcurrentGCThreadsafepoint_synchronize()を呼び出しているのがわかると思います。

share/vm/gc_implementation/shared/concurrentGCThread.cpp

57: void ConcurrentGCThread::safepoint_synchronize() {
58:   _sts.suspend_all();
59: }

58行目の_stsSuspendableThreadSetのことでした。suspend_all()を呼び出していますね。

次に、セーフポイントを終了するSafepointSynchronize::end()の一部を見てみましょう。

share/vm/runtime/safepoint.cpp

397: void SafepointSynchronize::end() {

480:     ConcurrentGCThread::safepoint_desynchronize();

今度はsafepoint_desynchronize()を呼び出しています。safepoint_desynchronize()の内部ではSuspendableThreadSetresume_all()を呼び出すだけです。

つまり、セーフポイントではSuspendableThreadSetを使って並行GCスレッドの動作を制御しています。並行GCスレッド群はセーフポイントになると、ルートを安全に列挙できる状態でyield()を呼び出し、自分自身を停止するわけです。

9.4 VMスレッド

HotspotVMではVMスレッドという特別なスレッドがたった1つだけ動いています。VMスレッドの役割は「VMオペレーション」というVM全体に関わる処理の要求を受け取り、VMスレッド上で実行するという点です。

VMスレッドとは?

VMスレッドはVMTreadクラスで定義されたスレッドです。VMThreadの祖先にはもちろんThreadクラスがいます。VMスレッドはJavaを起動してすぐに生成・起動します。

share/vm/runtime/vmThread.hpp

101: class VMThread: public NamedThread {

       // VMオペレーションの実行
128:   static void execute(VM_Operation* op);

VMスレッドはVMオペレーションを受け付けるキューを内部に保持しています。他スレッドは128行目のexecute()静的メンバ関数をVMオペレーションを引数に呼び出し、内部のキューに追加させます。VMスレッドはキューにVMオペレーションが追加されたことを検知して、自身のスレッドでVMオペレーションとして渡された処理を実施します。

VMオペレーション

VMオペレーションの代表的なものとしては、スタックトレースの取得や、VMの終了、VMヒープのダンプがあります。GCにもっとも関係のあるオペレーションはいわゆる「Stop-the-World」で実行しなければならない停止処理です。G1GCでいうところの退避や、並行マーキングの停止処理はVMオペレーションとしてVMスレッドに実行してもらいます。また、Javaで明示的にフルGCを実行する場合も停止処理ですのでVMオペレーションとなります。

VMオペレーションはセーフポイントで実行する必要があるものがほとんどです。そのためほとんどのVMオペレーション実行時には、VMスレッドはSafepointSynchronize::begin()を使ってセーフポイントの状態にもっていきます。

VM_Operationクラス

VM_OperationクラスがVMオペレーションのインターフェースを定義するクラスです。VM_Operationクラスの継承関係を図9.3に示します。

<tt class="inline-code">VM_Operation</tt>クラスの継承関係

図9.3: VM_Operationクラスの継承関係

VM_Operationクラスのインターフェースを見てみましょう。

share/vm/runtime/vm_operations.hpp

98: class VM_Operation: public CHeapObj {

       // VMスレッドが呼び出すメソッド
135:   void evaluate();

144:   virtual void doit()                            = 0;
145:   virtual bool doit_prologue()                   { return true; };
146:   virtual void doit_epilogue()                   {};

VMスレッドは135行目のevaluate()メンバ関数を呼び出し、要求されたオペレーションを実行します。evaluate()内部では単純に144行目のdoit()を呼び出すだけです。

144〜146行目には仮想関数が定義されています。doit()はオペレーションとしてVMスレッド上で実行される関数です。doit_prologue()は名前のとおり、doit()を実行する前の準備として実行されます。doit_prologue()は真偽値を返す決まりになっており、falseを返した場合はdoit()を実行しません。doit_epilogue()doit()が終わった後に実行される関数です。

VM_Operationを継承したクラスでは上記の3つのメンバ関数に対し、オペレーションとしての処理の内容を記述して行きます。

VMオペレーションの実行例

実際のVMオペレーション実行例を見てみましょう。ここではG1GCの並行マーキングの初期マークフェーズを見たいと思います。初期マークフェーズは停止処理ですので、VMオペレーションとして実行されます。

share/vm/gc_implementation/g1/concurrentMarkThread.cpp

134:         CMCheckpointRootsInitialClosure init_cl(_cm);
135:         strcpy(verbose_str, "GC initial-mark");
136:         VM_CGC_Operation op(&init_cl, verbose_str);
137:         VMThread::execute(&op);

136行目でVM_CGC_Operationをスタック上に生成し、execute()に渡してします。VMオペレーションのコンストラクタにはそれぞれのオペレーション内で利用するデータを渡します。この場合はCMCheckpointRootsInitialClosureと文字列だったようですね。

execute()を呼び出したスレッドはVMオペレーションが終了するまでブロックされます。VMオペレーションの種類によってはブロックされないこともありますが、それはとてもレアなケースですので本書では割愛します。


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

Webサイトのトップページ

(C) 2011-2012 Narihiro Nakamura