本章ではVMヒープに対して割り当てられるオブジェクトのデータ構造を見ていきます。割り当てられたオブジェクトは当然ながらGCの対象となります。
oopDescクラスはGC対象となるオブジェクトの抽象的な基底クラスです。oopDescクラスを継承したクラスのインスタンスがGC対象のオブジェクトとなります。
oopDescクラスの継承関係を図5.1に示します。
図5.1: oopDescクラスの継承関係
oopDescクラスには次のメンバ変数が定義されています。
share/vm/oops/oop.hpp
61: class oopDesc { 63: private: 64: volatile markOop _mark; 65: union _metadata { 66: wideKlassOop _klass; 67: narrowOop _compressed_klass; 68: } _metadata;
64行目の_mark変数はオブジェクトのヘッダとなる部分です。_mark変数にはマークスイープGC用のマークはもちろんのこと、そのほかのオブジェクトに必要なさまざまな情報が詰め込まれています。
oopDescは自分のクラスへのポインタを保持しています。それが65行目に共用体で定義されている_metadata変数です。この共用体にはほとんどの場合、66行目の_klass変数の値が格納されます。_klass変数はその名前のとおりオブジェクトのクラスへのポインタを格納します。67行目の_compressed_klassはGCとは関係ないため本書では特に触れません。
HotspotVMでは、oopDescインスタンスのポインタ(oopDesc*)などをtypedefで別名定義しています。
share/vm/oops/oopsHierarchy.hpp
42: typedef class oopDesc* oop; 43: typedef class instanceOopDesc* instanceOop; 44: typedef class methodOopDesc* methodOop; 45: typedef class constMethodOopDesc* constMethodOop; 46: typedef class methodDataOopDesc* methodDataOop; 47: typedef class arrayOopDesc* arrayOop; 48: typedef class objArrayOopDesc* objArrayOop; 49: typedef class typeArrayOopDesc* typeArrayOop; 50: typedef class constantPoolOopDesc* constantPoolOop; 51: typedef class constantPoolCacheOopDesc* constantPoolCacheOop; 52: typedef class klassOopDesc* klassOop; 53: typedef class markOopDesc* markOop; 54: typedef class compiledICHolderOopDesc* compiledICHolderOop;
Descを取り除いた名前にすべて別名定義されています。oopDescのDescは「Describe(表現)」の略です。つまり、oopDescとはoopという実体(オブジェクト)をクラスとして「表現」しているものなのです。本章ではoopDescなどのインスタンスを別名定義のルールに従ってoopのように呼ぶことにします。
oopDescクラスを継承しているklassOopDescクラスはJava上のクラスを表すクラスです。つまり、Java上の「java.lang.String」は、VM上ではklassOopDescクラスのインスタンス(klassOop)となります。クラス名の一部が「class」ではなく「klass」となっているのは、C++の予約語と区別するためです。このテクニックは多くの言語処理系でよく見かけます。
前の「5.1 oopDescクラス」の項で説明した通り、全オブジェクトはklassOopを持っています。またklassOopDesc自体もoopDescを継承しているため、klassOopをメンバ変数として持っています。
klassOopの特徴は内部にKlassクラスのインスタンスを保持しているということです。実はklassOopDescクラス自体に情報はほとんどなく、klassOopは内部にKlassインスタンスを保持するただの箱にすぎません。
Klassクラスは名前のとおり、型情報を保持しています。KlassのインスタンスはklassOopの一部として生成されます。
Klassはさまざまな型情報の抽象的な基底クラスです。Klassの継承関係を図5.2に示します。
図5.2: Klassクラスの継承関係
Klassの子クラスには、oopDescの子クラスと対応するクラスが存在します。そのようなXXDescのインスタンスには、XXDescに対応したXXKlassを保持するklassOopが格納されます。
前の「5.2 klassOopDescクラス」の項でklassOopはただの箱だといいました。klassOopはオブジェクトとしてKlassやその子クラスを統一的にあつかうためのインタフェースだといえるでしょう。つまり、外側はklassOopだとしても内部にはinstanceKlassやsymbolKlassなどが入っているということです(図5.3)。
図5.3: klassOopはKlassの箱
では、1つのオブジェクトを例にとってoopとKlassの関係を具体的に見ていきましょう。
次のようなStringクラスのオブジェクトを生成するJavaプログラムがあったとします。
リスト5.1: Stringオブジェクトを生成するJavaプログラム
1: String str = new String(); 2: System.out.println(str.getClass()); // => java.lang.String 3: System.out.println(str.getClass().getClass()); // => java.lang.Class 4: System.out.println(str.getClass().getClass().getClass()); // => java.lang.Class
その場合、str変数にはStringクラスのオブジェクトが格納されます。この時、HotspotVM上でのoopとKlassの関係は図5.4のようになっています。
図5.4: Stringオブジェクトのoop
instanceOopはJava上のインスタンスへの参照と同じ意味を持ちます。図5.4左端のinstanceOopは、「new String()」の評価時に生成されたinstanceOopDescのインスタンスへのポインタを示しています。
instanceOopは自身の_klass変数(オブジェクトのクラスを示す変数)にklassOopをもちます。そして、そのklassOopの中にはinstanceKlassのインスタンスが格納されます。図5.4中央のklassOopは、リスト5.1の2行目で示したJava上のStringクラスと対応しています。
次に、図中央のklassOop(=Java上のStringクラス)は内部にさらに別のklassOopを持ちます。このklassOopの中にはinstanceKlassKlassのインスタンスが格納されています。図5.4右端のklassOopが上記を表しており、リスト5.1の3行目で示したJava上のClassクラスと対応しています。
instanceKlassKlassはinstanceKlassのクラスを示します。instanceKlassKlassをもつklassOopは自分自身を_klassにもち、instanceOopから続くクラスの連鎖を収束させる役割を持っています。リスト5.1を見ると3行目のgetClass()メソッドの結果と、4行目のgetClass()メソッドの結果が同じ値になっています。これはクラスの連鎖がinstanceKlassKlassのところでループしているためです。
oopDescクラスにはC++の仮想関数(virtual function)*1を定義してはいけない決まりになっています。
クラスに仮想関数を定義するとC++のコンパイラがそのクラスのインスタンスに仮想関数テーブル(vtable)*2へのポインタを勝手に付けてしまいます。もしoopDescに仮想関数を定義するとすべてのオブジェクトに対して1ワードが確保されてしまいます。これは空間効率が悪いので、oopDescクラスにはC++の仮想関数を定義できないルールとなっています。
もし、仮想関数を使って子クラスごとに違う振る舞いをするメンバ関数を定義したい場合はoopDescではなく、対応するKlassの方に仮想関数を定義しなければなりません。
次にその一部を示します。ここでは自分がJava上でどのような意味をもつオブジェクトかを判断する仮想関数が定義されています。
share/vm/oops/klass.hpp
172: class Klass : public Klass_vtbl { // Java上の配列か? 582: virtual bool oop_is_array() const { return false; }
上記のメンバ関数はKlassを継承するクラスで次のように再定義されます。
shara/vm/oops/arrayKlass.hpp
35: class arrayKlass: public Klass { 47: bool oop_is_array() const { return true; }
そして、oopDescクラスではKlassの仮想関数を呼び出します。
share/vm/oops/oop.inline.hpp
139: inline Klass* oopDesc::blueprint() const { return klass()->klass_part(); } 146: inline bool oopDesc::is_array() const { return blueprint()->oop_is_array(); }
139行目のblueprint()はklassOopの中からKlassのインスタンスを取り出すメンバ関数です。146行目ではKlassの仮想関数で定義されたメンバ関数を呼び出しています。oopのis_array()を呼び出しても、対応したKlassのoop_is_array()が応答し、falseが戻りますが、arrayOopのis_array()を呼び出すと、対応したarrayKlassのoop_is_array()が応答し、trueが戻ります。
Klassに仮想関数を定義するため、klassOopには仮想関数テーブルへのポインタが付いてしまいますが、Java上のクラスはオブジェクトよりも全体量が少ないため問題になりません。
「5.1 oopDescクラス」で少し取り上げたオブジェクトのヘッダについてもう少し説明しておきましょう。オブジェクトのヘッダはmarkOopDescクラスで表現されます。
ヘッダ内の主な情報として次のものが詰め込まれます。
ヘッダを表すmarkOopDescクラスはかなりトリッキーなコードになっています。著者はC++にあまり馴染みがないので「こんな書き方もできるのか…」と驚かされました。
markOopDescは1ワードのデータだけをヘッダとして利用するクラスです。markOopDescの利用イメージを次に示します。
リスト5.2: markOopDescの利用イメージ
1: markOopDesc* header; 2: uintptr_t some_word = 1; 3: 4: header = (markOopDesc*)some_word; 5: header->is_marked(); // マーク状態を調べる
1行目でmarkOopDesc*のローカル変数を定義し、2行目ではuintptr_t、つまり1ワードのデータをローカル変数で定義しています。
4行目でそれをmarkOopDesc*にキャストし、5行目で関数呼び出し…。さて、some_wordは1だったはずです。つまり、5行目では1というアドレス上のデータをインスタンスとみなして関数呼び出しているのですから、SEGVが起こってもおかしくないような…?
実はmarkOopDescクラス自体はインスタンスを生成せず、自身のアドレス(this)しか利用しないように実装されています。自身のアドレスをヘッダ用の情報として利用するクラスなのです。
share/vm/oops/markOop.hpp
104: class markOopDesc: public oopDesc { 105: private: 107: uintptr_t value() const { return (uintptr_t) this; } 221: bool is_marked() const { 222: return (mask_bits(value(), lock_mask_in_place) == marked_value); 223: }
107行目のvalue()というthisを返すものをベースにさまざまなメンバ関数が実装されています。利用例として221〜223行目にis_marked()というマーク済みかどうかを返すメンバ関数をみてみましょう。222行目ではvalue()で得た1ワードデータをマスクし、マークビットが立っているかどうかを判断して結果を返しています。
さて、104行目でmarkOopDescはoopDescクラスを一応継承していますが、これはまったく利用しません。oopDescからいくらかのメンバ変数も継承しますが、markOopDescはインスタンスを持たないのでこれらも使うことができません。じゃあ、なぜこんな余計なクラスを継承しているのか、という話ですが、きちんとコメントが書いてありました。
share/vm/oops/markOop.hpp
32: // Note that the mark is not a real oop but just a word. 33: // It is placed in the oop hierarchy for historical reasons. // (訳) // markはほんとうのoopではなくただのワードであることに注意してください。 // これがoopの継承関係にいるのは歴史的な理由によるものです。
なるほど。うん、しょうがないか。歴史的な理由ならしょうがないですね。
個人的にはこんな複雑なことはせずに、単純に1ワードのメンバ変数をもつようなクラスを定義すればいいのではと思います。C++だとコンパイラが余計なデータ領域を確保したりすることがあるので嫌なのでしょうか(vtableなど)。でも、これは読みづらいですよね…。
オブジェクトヘッダの使い方の実例として、コピーGCに利用されるフォワーディングポインタとしての利用方法を見てみましょう。
以下にG1GCのオブジェクトをコピーするメンバ関数を示します。
share/vm/gc_implementation/g1/g1CollectedHeap.cpp
4369: oop G1ParCopyHelper::copy_to_survivor_space(oop old) { 4370: size_t word_sz = old->size(); 4382: HeapWord* obj_ptr = _par_scan_state->allocate(alloc_purpose, word_sz); 4383: oop obj = oop(obj_ptr); 4395: oop forward_ptr = old->forward_to_atomic(obj); 4396: if (forward_ptr == NULL) { // オブジェクトのコピー 4397: Copy::aligned_disjoint_words((HeapWord*) old, obj_ptr, word_sz); 4457: } 4458: return obj; 4459: }
引数にはコピー元のオブジェクトへのポインタ(old)を受け取ります。4370行目でオブジェクトのサイズを取得し、サイズ分のオブジェクトを4382行目で新たに割り当てます。4383行目で割り当てた領域に対するアドレスをoopにキャストします。
4383行目のoop(p)の部分は若干説明が必要でしょう。C++では関数呼び出しと同様の構文で明示的な型変換をおこなえます。キャストと明示的な型変換は複数の引数が受け取れる箇所が異なります。キャストの場合は引数が実質1つしか受け取れませんが、明示的な型変換は複数の引数を受け取れます。ただ、oop(p)の場合は引数を1つしか受け取っていませんので、(oop)pと同じことだと考えればいいでしょう。
4395行目のforward_to_atomic()がフォワーディングポインタを作成するメンバ関数です。このメンバ関数は並列に動く可能性があり、途中でほかのスレッドに割り込まれて先にコピーされてしまった場合はNULLを返します。無事、フォワーディングポインタを設定できたら、4397行目で実際にオブジェクトの内容をコピーし、4458行目でコピー先のアドレスを返します。
では、forward_to_atomic()メンバ関数の中身を見てみましょう。
share/vm/oops/oop.pcgc.inline.hpp
76: inline oop oopDesc::forward_to_atomic(oop p) { 79: markOop oldMark = mark(); 80: markOop forwardPtrMark = markOopDesc::encode_pointer_as_mark(p); 81: markOop curMark; 86: while (!oldMark->is_marked()) { 87: curMark = (markOop)Atomic::cmpxchg_ptr( forwardPtrMark, &_mark, oldMark); 89: if (curMark == oldMark) { 90: return NULL; 91: } 95: oldMark = curMark; 96: } 97: return forwardee(); 98: }
79行目でコピー元のオブジェクトのヘッダをローカル変数のoldMarkに格納します。次に80行目でコピー先のアドレスをフォワーディングポインタにエンコードします。この静的メンバ関数の中身は後述します。
その後、86〜96行目で、CAS命令を利用して不可分にフォワーディングポインタをコピー元のオブジェクトの_markに書き込みます。97行目のforwardee()でフォワーディングポインタをデコードし、呼び出し元に返します。この関数の役割は後述するマークビットを外すだけしかありません。
以下にフォワーディングポインタをエンコードするencode_pointer_as_mark()を示します。
share/vm/oops/markOop.hpp
363: inline static markOop encode_pointer_as_mark(void* p) { return markOop(p)->set_marked(); }
受け取ったポインタをmarkOopにキャストして、set_marked()を呼び出しているだけです。
share/vm/oops/markOop.hpp
158: enum { locked_value = 0, // ... 161: marked_value = 3, // ... 163: }; 333: markOop set_marked() { return markOop((value() & ~lock_mask_in_place) | marked_value); }
set_marked()は下位2ビットを1にする(マークビットを立てる)メンバ関数です。オブジェクトアドレスの下位2ビットは必ず0になるようにアラインメントされることを利用したハックですね。
上記のマークによって、そのオブジェクトがすでにコピーされているか判断できます。
share/vm/oops/oop.inline.hpp
641: inline bool oopDesc::is_forwarded() const { 644: return mark()->is_marked(); 645: }
G1GCやそのほかのコピーGCでは上記のis_forwarded()を確認し、マークが付いているものは再度コピーしないようにします。
御意見・御感想・誤植の指摘などは@nari3もしくはauthorNari/g1gc-impl-book - GitHubまでお願いします。
(C) 2011-2012 Narihiro Nakamura