5 Java HotSpot仮想マシン・パフォーマンス向上
この章では、OracleのHotSpot仮想マシン・テクノロジにおけるパフォーマンスの強化について説明します。
コンパクト文字列
コンパクト文字列機能は、文字列の内部表現のスペース効率を高めます。
異なるアプリケーションからのデータは、文字列がJavaヒープの使用における主要な構成要素であること、および大半のjava.lang.String
オブジェクトにLatin-1文字のみが含まれていることを示唆しています。そのような文字は、記憶域を1バイトのみ必要とします。結果として、java.lang.String
オブジェクトの内部文字配列のスペースの半分は使用されません。Java SE 9で採用されたコンパクト文字列機能は、メモリー・フットプリントを削減し、ガベージ・コレクション・アクティビティを削減します。アプリケーションでパフォーマンス低下の問題が確認された場合には、この機能を無効にすることができます。
コンパクト文字列機能によって、新しく公開されたAPIやインタフェースは導入されません。これにより、java.lang.String
クラスの内部表現が、UTF-16 (2バイト)文字配列から、文字のエンコードを識別する追加フィールドを含む1バイトの配列に変更されます。その他の文字列関連のクラス(AbstractStringBuilder
、StringBuilder
およびStringBuffer
など)は、同様の内部表現を使用するように更新されます。
Java SE 9では、コンパクト文字列機能がデフォルトで有効になっています。したがって、java.lang.String
クラスは、1文字当たり1バイトとして、Latin-1にエンコードされた文字を格納します。追加の文字エンコード・フィールドは、使用されているエンコードを示します。HotSpot VM文字列の組込みは、内部表現をサポートするように更新および最適化されます。
コンパクト文字列機能は、java
コマンドラインで-XX:-CompactStrings
フラグを使用して無効にすることができます。この機能を無効にすると、java.lang.String
クラスは、UTF-16でエンコードされた2バイトとして文字を格納し、UTF-16エンコーディングを使用するためのHotSpot VM文字列を格納します。
階層型コンパイル
Java SE 7で導入された階層型コンパイルによって、サーバーVMにクライアントVMの起動速度をもたらします。階層型コンパイルなしの場合、サーバーVMはインタプリタを使用して、コンパイラに送信されるメソッドについてのプロファイリング情報を収集します。階層型コンパイルでは、サーバーVMでもクライアント・コンパイラを使用して、メソッドのコンパイル・バージョンを生成し、それらが自身のプロファイリング情報を収集します。コンパイルされたコードはインタプリタよりもはるかに高速なため、プログラムのプロファイリング段階の実行パフォーマンスが大きく向上します。サーバー・コンパイラによって生成される最終コードをアプリケーション初期化の早い段階で利用できる可能性があるため、多くの場合クライアントVMの起動速度より起動が速くなります。また、階層型コンパイルではプロファイリング段階の高速化によってプロファイリングにかけられる時間が長くなるため、通常のサーバーVMよりも優れたピーク・パフォーマンスを実現でき、最適化の向上にもつながる可能性があります。
階層型コンパイルはサーバーVMではデフォルトで有効になっています。64ビット・モードおよび圧縮Ordinary Object Pointerがサポートされています。階層型コンパイルを無効にするには、java
コマンドで-XX:-TieredCompilation
フラグを使用します。
階層型コンパイルで生成される追加のプロファイリング・コードを格納するために、コード・キャッシュのデフォルト・サイズは、5xで乗算されます。大きいスペースを効率的に編成および管理するために、セグメント化されたコード・キャッシュが使用されます。
セグメント化されたコード・キャッシュ
コード・キャッシュは、Java仮想マシンが生成されたネイティブ・コードを格納するメモリー領域です。連続したメモリー・チャンクの最上位の単一ヒープ・データ構造として編成されます。
1つのコード・ヒープを持つかわりに、コード・キャッシュはセグメントに分割され、それぞれに特定のタイプのコンパイル済コードが含まれます。このセグメンテーションによって、JVMメモリー・フットプリントをより効果的に制御でき、コンパイル済メソッドのスキャン時間が短縮され、コード・キャッシュの断片化が大幅に減少し、パフォーマンスが向上します。
コード・キャッシュは、次の3つのセグメントに分割されています。
表5-1 セグメント化されたコード・キャッシュ
コード・キャッシュのセグメント | 説明 | JVMコマンド・ライン引数 |
---|---|---|
非メソッド |
このコード・ヒープには、コンパイラ・バッファやバイトコード・インタプリタなどの非メソッド・コードが含まれます。このコード・タイプは、コード・キャッシュに永続的に留まります。コード・ヒープには3MBの固定サイズがあり、残りのコード・キャッシュはプロファイル済および非プロファイルのコード・ヒープ間で均等に分散されます。 |
|
プロファイル済 |
このコード・ヒープには、存続期間が短い、少し最適化されたプロファイル済メソッドが含まれています。 |
|
非プロファイル |
このコード・ヒープには、存続期間が潜在的に長い、十分に最適化された非プロファイル・メソッドが含まれています。 |
|
Graal : JavaベースのJITコンパイラ
Graalは、Javaで記述された高パフォーマンスの最適化just-in-timeコンパイラで、Java HotSpot VMと統合されています。Javaから起動できるカスタマイズ可能な動的コンパイラです。
Graalの機能および利点の一部を次に示します。
-
柔軟な推論的最適化
-
適切なインライン化
-
部分的なエスケープ分析
-
JavaツールおよびIDEサポートによる利点
-
より強力なコード生成制御を可能にするメタサーキュラ・アプローチ
Graalを静的コンテキストで使用することもできます。静的Ahead of Timeコンパイラは、Graalフレームワークに基づいています。
GraalはJDKビルドの一部であり、内部モジュールjdk.internal.vm.compiler
として配布されます。JVM Compiler Interface (JVMCI)を使用してJVMと通信します。JVMCIもJDKビルドの一部であり、内部モジュールjdk.internal.vm.ci
に含まれています。
JITコンパイラとしてGraalを有効にするには、java
コマンドラインで次のオプションを使用します。
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
ノート:
Graalは実験的な機能であり、Linux-x64でのみサポートされています。Ahead-of-Timeコンパイル
Ahead-of-time (AOT)コンパイルでは、仮想マシンを起動する前にJavaクラスをネイティブ・コードにコンパイルすることで、小規模および大規模なアプリケーションの起動時間を短縮します。
just-in-time (JIT)コンパイラは高速ですが、大きいJavaプログラムのコンパイルには時間がかかります。また、コンパイルされていない特定のJavaメソッドが繰り返し解釈されると、パフォーマンスが影響を受けます。AOTがこれらの問題に対処しています。
jaotc
がAOTコンパイルに使用されます。jaotc
ツールの構文は次のとおりです。 jaotc <options> <list of classes or jar files>
jaotc <options> <--module name>
次に例を示します。
jaotc --output libHelloWorld.so HelloWorld.class
jaotc --output libjava.base.so --module java.base
jaotc
ツールはJavaインストールの一部であり、javac
に似ています。
java -XX:AOTLibrary=./libHelloWorld.so,./libjava.base.so HelloWorld
JVMの起動時に、AOT初期化コードは、AOTLibrary
フラグを使用して指定したライブラリを検索します。ライブラリが見つからない場合、AOTはそのJVMインスタンスでオフになります。
jaotc
ツールの詳細は、Java Development Kitツール仕様を参照してください。
ノート:
Ahead-of-Time (AOT)コンパイルは実験的な機能であり、Linux-x64でのみサポートされています。圧縮Ordinary Object Pointer
Java HotSpotの専門用語であるOrdinary Object Pointer (OOP)は、オブジェクトへの管理ポインタです。OOPは通常、ネイティブ・マシン・ポインタと同じサイズ(LP64システムでは64ビット)です。ILP32システムでは、最大ヒープ・サイズは4Gバイトより少なく、これは多くのアプリケーションにとって十分ではありません。LP64システムでは、指定されたプログラムが使用するヒープが、ILP32システムで実行する場合よりも約1.5倍大きくなることがあります。これほど必要なのは、管理ポインタのサイズが増大することが原因です。メモリーのコストは低いですが、最近では帯域幅およびキャッシュが不足しているため、4Gバイト制限を解決するためだけにヒープ・サイズが大幅に増加するのは望ましくありません。
Javaヒープ内の管理ポインタは、8バイト・アドレス境界に整列されたオブジェクトを指します。圧縮OOPは、(Java仮想マシン(JVM)ソフトウェア内のすべてではないものの多くの場所で)管理ポインタを、64ビットJavaヒープ・ベース・アドレスからの32ビット・オブジェクト・オフセットとして表します。これらはバイト・オフセットではなくオブジェクト・オフセットのため、最大40億のオブジェクト(バイトではありません)、または最大約32Gバイトのヒープ・サイズをアドレス指定するために使用できます。これらを使用して参照先オブジェクトを見つけるには、これらを8倍してJavaヒープ・ベース・アドレスに加算する必要があります。圧縮OOPを使用するオブジェクト・サイズは、ILP32モードのそれに匹敵します。
デコードという語は、32ビット圧縮OOPが64ビット・ネイティブ・アドレスに変換され、管理ヒープ内に追加される処理を表します。エンコードという語は、その逆の処理を表します。
圧縮OOPはJava SE 6u23以降でサポートされ、デフォルトで有効になっています。Java SE 7では、-Xmx
が指定されていないときの64ビットJVMプロセスおよび-Xmx
の値が32Gバイト未満の場合に、圧縮OOPがデフォルトで有効になります。6u23リリースより前のJDKリリースの場合は、java
コマンドで-XX:+UseCompressedOops
フラグを使用して、圧縮OOPを有効にします。
ゼロ・ベース圧縮Ordinary Object Pointer
64ビットJVMプロセスで圧縮Ordinary Object Pointers (OOP)を使用する場合、JVMソフトウェアは、仮想アドレス・ゼロから始まるメモリーをJavaヒープ用に予約するようにオペレーティング・システムに要求を送信します。オペレーティング・システムがこのような要求をサポートしていて、Javaヒープ用のメモリーを仮想アドレス・ゼロで予約できる場合、ゼロ・ベース圧縮OOPが使用されます。
ゼロ・ベース圧縮OOPを使用する場合、Javaヒープ・ベース・アドレスを含めることなく、32ビット・オブジェクト・オフセットから64ビット・ポインタをデコードできます。ヒープ・サイズが4Gバイト未満の場合、JVMソフトウェアはオブジェクト・オフセットの代わりにバイト・オフセットを使用できるため、オフセットを8倍にすることも回避できます。64ビット・アドレスを32ビット・オフセットにエンコードすると、それだけ効率的になります。
Javaヒープ・サイズが26ギガバイトまでの場合は、LinuxおよびWindowsオペレーティング・システムでは通常、Javaヒープを仮想アドレス・ゼロに割り当てることができます。
エスケープ解析
エスケープ解析は、Java HotSpot Serverコンパイラが新規オブジェクトの使用スコープを解析し、そのオブジェクトをJavaヒープに割り当てるかどうかを決定するための技術です。
エスケープ解析はJava SE 6u23以降でサポートされ、デフォルトで有効になっています。
Java HotSpot Serverコンパイラは、次に記述する、フローインセンシティブ・エスケープ解析アルゴリズムを実装します。
[Choi99] Jong-Deok Choi, Manish Gupta, Mauricio Seffano,
Vugranam C. Sreedhar, Sam Midkiff,
"Escape Analysis for Java", Procedings of ACM SIGPLAN
OOPSLA Conference, November 1, 1999
エスケープ解析に基づき、オブジェクトのエスケープ状態は次のいずれかになる可能性があります。
GlobalEscape
: オブジェクトはメソッドおよびスレッドをエスケープします。たとえば、staticフィールドに格納された、またはエスケープされたオブジェクトのフィールドに格納された、または現在のメソッドの結果として返されたオブジェクトなど。ArgEscape
: オブジェクトは引数として渡されるか、または引数によって参照されるけれども、呼出し中にグローバルにエスケープしません。この状態は、呼び出されるメソッドのバイト・コードを解析することで判断されます。NoEscape
: オブジェクトはスカラー置換可能なオブジェクトです。これは、生成されたコードからその割当てを削除できることを意味します。
エスケープ解析の後、サーバー・コンパイラは、スカラー置換可能オブジェクト割当ておよび関連付けられたロックを、生成されたコードから除去します。また、サーバー・コンパイラはグローバルにエスケープしないオブジェクトのロックも除去します。ヒープ割当てを、グローバルにエスケープしないオブジェクトのスタック割当てで置き換えることはありません。
次の例で、エスケープ解析についていくつかのシナリオを示します。
-
サーバー・コンパイラはある種のオブジェクト割り当てを除去する場合があります。たとえば、メソッドがオブジェクトの防衛的コピーを作成してそのコピーを呼出し側に返すとします。
public class Person { private String name; private int age; public Person(String personName, int personAge) { name = personName; age = personAge; } public Person(Person p) { this(p.getName(), p.getAge()); } public int getName() { return name; } public int getAge() { return age; } } public class Employee { private Person person; // makes a defensive copy to protect against modifications by caller public Person getPerson() { return new Person(person) }; public void printEmployeeDetail(Employee emp) { Person person = emp.getPerson(); // this caller does not modify the object, so defensive copy was unnecessary System.out.println ("Employee's name: " + person.getName() + "; age: " + person.getAge()); } }
メソッドは、呼出し側が元のオブジェクトを変更するのを防ぐためにコピーを作成します。コンパイラは
getPerson
メソッドがループ内で呼び出されていると判断すると、そのメソッドをインライン化します。コンパイラはエスケープ解析を使用して元のオブジェクトが変更されていないと判断すると、コピーを作成する呼出しを最適化して除去することができます。 -
サーバー・コンパイラは、オブジェクトがスレッド・ローカルであると判断すると、同期ブロックを除去する場合があります(ロック除去)。たとえば、
StringBuffer
やVector
などのクラスのメソッドは、別のスレッドからアクセスできるため同期されています。ただし、ほとんどのシナリオでは、これらはスレッド・ローカルで使用されます。スレッド・ローカルで使用される場合、コンパイラは同期ブロックを最適化して除去することができます。