HotSpotの専門用語である"OOP"("Ordinary Object Pointer")は、オブジェクトに対する管理ポインタを表します。 これは通常、ネイティブ・マシンのポインタと同じサイズになります。つまり、LP64システムでは64ビットです。 ILP32システムでは、最大ヒープ・サイズは4GBよりやや少ない程度です。これは多くのアプリケーションにとって十分ではありません。 一方、LP64システムでは、実行時のヒープは対応するILP32システムの約1.5倍必要となることもあります(どちらのモードも同じ状況と仮定した場合)。 これは、管理ポインタのサイズが増大することが原因となっています。 メモリのコストは低いですが、最近では帯域幅およびキャッシュが不足しているため、4GBという制限を解決するためだけにヒープ・サイズが大幅に増加してしまうのは厳しいところです。
(さらに、x86チップの場合、ILP32モードで利用できるレジスタはLP64モードの半分しかありません。 SPARCはこのような影響を受けません。RISCチップはもともとレジスタ数が多く、LP64モードでは単にレジスタ幅が拡大しています。)
圧縮OOPは(JVMのすべてではないものの、多くの領域で)32ビットの値で管理ポインタを表します。この管理ポインタの参照先のオブジェクトを見つけるには、この値を8倍して64ビット・ベースのアドレスに加算する必要があります。 これによって、アプリケーションは最大40億のオブジェクト(バイトではありません)、または最大約32GBのヒープ・サイズに対応できます。 同時に、データ構造はILP32モードと匹敵するほどにコンパクトです。
ここでは、32ビットの圧縮OOPが管理ヒープ内の64ビットのネイティブ・アドレスに変換される処理を、デコードという語で表します。 この逆の処理は、エンコードです。
ILP32モードのJVMの場合、またはLP64モードでUseCompressedOopsフラグをオフにした場合、すべてのOOPはネイティブ・マシンのワード・サイズとなります。
UseCompressedOopsがtrueの場合は、ヒープ内の次のOOPが圧縮されます。
Javaクラスを管理するHotspot VMのデータ構造は圧縮されません。 これらのデータ構造は一般的に、Permanent Generation(PermGen)として知られるJavaヒープ領域に存在します。
インタプリタでは、OOPが圧縮されることはありません。 このOOPには、JVMローカルおよびスタック要素、外部への呼出し引数、戻り値が含まれます。 インタプリタはヒープから読み込まれたOOPをしきりにデコードし、OOPをヒープに保存する前にエンコードします。
同様に、メソッドの呼出しシーケンスでは、インタプリタの場合でもコンパイルの場合でも、圧縮したOOPは使用しません。
コンパイル済みコードでは、各種最適化の結果によって、OOPが圧縮される場合とされない場合があります。 最適化されたコードが、管理ヒープ内のある場所から別の場所に、圧縮OOPをデコードせずに移動できる可能性があります。 同様に、チップ(x86など)でデコード処理に使用できるアドレッシング・モードがサポートされている場合は、オブジェクト・フィールドや配列要素を指定するために使用される圧縮OOPでも、圧縮OOPはデコードされない場合があります。
そのため、コンパイル済みコード内の次の構造は、圧縮OOPかネイティブ・ヒープ・アドレスのいずれかを参照できます。
HotSpot JVMのC++コードでは、圧縮OOPとネイティブOOPの違いをC++の静的な型のシステムに反映しています。 一般的に、多くの場合OOPは圧縮されていません。 特にC++メンバー関数は、通常どおりネイティブ・マシン・ワードで表されたレシーバ(this)上で処理を行います。 JVMのいくつかの関数は、圧縮OOPとネイティブOOPのいずれでも対応できるようにオーバーロードされます。
以下の重要なC++値は圧縮されることはありません。
C++コードには、圧縮OOPを操作(通常はロードまたは保存)している場所をマークするnarrowOopという型があります。
圧縮OOPを使用するx86の命令シーケンスの例は、次のとおりです。
! int R8; oop[] R9; // R9 is 64 bits ! oop R10 = R9[R8]; // R10 is 32 bits ! load compressed ptr from wide base ptr: movl R10, [R9 + R8<<3 + 16] ! klassOop R11 = R10._klass; // R11 is 32 bits ! void* const R12 = GetHeapBase(); ! load compressed klass ptr from compressed base ptr: movl R11, [R12 + R10<<3 + 8]
圧縮OOP(nullの可能性もあるもの)をデコードするSPARCの命令シーケンスの例は、次のとおりです。
! java.lang.Thread::getThreadGroup@1 (line 1072) ! L1 = L7.group ld [ %l7 + 0x44 ], %l1 ! L3 = decode(L1) cmp %l1, 0 sllx %l1, 3, %l3 brnz,a %l3, .+8 add %l3, %g6, %l3 ! %g6 is constant heap base
(注釈付きの出力は、逆アセンブル・プラグインによるものです。)
32ビットのゼロ値は、64ビットのネイティブのnull値にデコードされます。 このため、デコードのロジックに特別なパスを追加する必要があります。たとえば、nullにはならないと保証された圧縮OOP(klassフィールドなど)を静的に定め、よりシンプルな完全デコード/エンコード処理を使用すると効果的となります。
暗黙的なnullチェックは、インタプリタによるバイトコードでもコンパイルによるバイトコードでも、JVMのパフォーマンスにとって極めて重要です。 十分小さなオフセットをベース・ポインタに対して使用しメモリを参照すると、ベース・ポインタがnullの場合は、確実に何らかのトラップやシグナルが発生します。これは、仮想アドレス空間の最初のページなどはマップされないからです。
圧縮OOPでも同様の技術を使用できることがあります。この場合、管理ヒープに使用される仮想アドレスの最初のページなどのマップを解除します。 考え方としては、圧縮後のnullを(シフトとヒープ・ベースの加算によって)デコードした場合でも、ロードまたは保存の処理で使用できるようになります。この場合でも、コードは暗黙nullチェックを利用できます。
オブジェクト・ヘッダーは、ネイティブサイズのmarkワード、klassワード、32ビットのlengthワード(オブジェクトが配列の場合)、32ビットのgap(配置ルールで必要な場合)、さらには0個以上のインスタンス・フィールド、配列要素、またはメタデータ・フィールドで構成されています。 (興味深いトリビア: Klassメタオブジェクトには、klassワードのすぐ後にC++ vtableが含まれています。)
gapフィールドが存在すると、多くの場合、インスタンス・フィールドの保存に使用できます。
UseCompressedOopsがfalseの場合(かつ常にILP32システムにある場合)、markとklassは両方ともネイティブ・マシン・ワードです。 配列については、gapはLP64システム上では必ず存在します。また、ILP32システム上では64ビット要素の配列でのみ存在します。
UseCompressedOopsがtrueの場合、klassは32ビットです。 非配列の場合は、klassのすぐ後にgapフィールドがあり、配列の場合はklassのすぐ後にlengthフィールドが保存されます。
圧縮OOPでは、narrow oop baseに対して任意のアドレスが使用されます。これは、暗黙nullチェックが動作するように、Javaヒープ・ベースから(保護された)ページ・サイズ1つ分を除算したものとして計算されます。 つまり、通常のフィールド参照は次のようになります。
<narrow-oop-base> + (<narrow-oop> << 3) + <field-offset>.
narrow oop baseを0にできる場合(Javaヒープが実際はオフセット0で開始する必要はない)、通常のフィールド参照は単純に次のようになります。
(<narrow-oop << 3) + <field-offset>
理論的には、これによってヒープ・ベースの加算を節約できるようになります(現在のレジスタ・アロケータでは、レジスタを保存できません)。 また、ゼロベースによって、圧縮OOPのnullチェックが必要なくなります。
圧縮OOPをデコードするための現在のコードは、次のようになっています。
if (<narrow-oop> == NULL) <wide_oop> = NULL else <wide_oop> = <narrow-oop-base> + (<narrow-oop> << 3)
ゼロのnarrow oop base を使用すると、コードははるかにシンプルになります。 圧縮OOPのデコード/エンコードにシフト処理が必要になるだけです。
<wide_oop> = <narrow-oop> << 3
また、Javaヒープ・サイズが4GB未満であり、低位の仮想アドレス空間(4GB未満)に持っていける場合は、圧縮OOPはエンコード/デコードなしで使用できます。
ゼロベースの実装では、Javaヒープ・サイズおよび稼働するプラットフォームによって異なる戦略を使用して、Javaヒープを割り当てようとします。
まず、ヒープ・サイズが4GB未満の場合は、Javaヒープを4GB未満に割り当てて、デコードなしで圧縮OOPを使用するよう試みます。
これが失敗するか、ヒープ・サイズが4GBを超える場合は、32GB未満にヒープを割り当てて、ゼロベース圧縮OOPを使用するよう試みます。
さらにこれが失敗する場合は、narrow oop baseを使用した通常の圧縮OOPに切り替えます。