この章では、Java HotSpot仮想マシン・テクノロジにおけるパフォーマンスの強化について説明します。
コンパクト文字列は、文字列の内部表現のスペース効率を高める機能です。
異なるアプリケーションからのデータは、文字列がJavaヒープの使用における主要な構成要素であること、および大半のjava.lang.StringオブジェクトにLatin-1文字のみが含まれていることを示唆しています。そのような文字は、記憶域を1バイトのみ必要とします。結果として、java.lang.Stringオブジェクトの内部文字配列のスペースの半分は使用されません。Java SE 9で採用されたコンパクト文字列機能は、メモリー・フットプリントを削減するだけでなく、ガベージ・コレクション・アクティビティの削減も実現します。アプリケーションでパフォーマンス低下の問題が確認された場合には、この機能を無効にすることができます。
コンパクト文字列機能は、java.lang.Stringクラスの内部表現をUTF-16 (2バイト)文字配列から、文字のエンコードを識別する追加フィールドを含む1バイトの配列に変更します。その他の文字列関連のクラス(AbstractStringBuilder、StringBuilderおよびStringBufferなど)は、同様の内部表現を使用するように更新されます。コンパクト文字列機能は、新たな公開APIまたはインタフェースを採用せず、純粋に文字列の内部表現を変更します。
Java SE 9では、コンパクト文字列機能がデフォルトで有効になっています。したがって、java.lang.Stringクラスは、1文字当たり1バイトとして、Latin-1にエンコードされた文字を格納します。追加の文字エンコード・フィールドは、使用されているエンコードを示します。HotSpot VM文字列の組込みは、内部表現をサポートするように更新および最適化されます。
コンパクト文字列機能は、javaコマンドラインで-XX:-CompactStringsフラグを使用して無効にすることができます。この機能を無効にすると、java.lang.Stringクラスは、UTF-16でエンコードされた文字を2バイトとして格納します。これにより、HotSpot VM文字列の組込みはUTF-16エンコードを使用するように戻されます。
Java SE 7で導入された階層型コンパイルによって、サーバーVMにクライアントVMの起動速度をもたらします。階層型コンパイルなしの場合、サーバーVMはインタプリタを使用して、コンパイラに送信されるメソッドについてのプロファイリング情報を収集します。階層型コンパイルでは、サーバーVMはインタプリタに加えてクライアント・コンパイラを使用して、メソッドのコンパイル・バージョンを生成し、それらが自身のプロファイリング情報を収集します。コンパイルされたコードはインタプリタよりもはるかに高速なため、プログラムのプロファイリング段階の実行パフォーマンスが大きく向上します。サーバー・コンパイラによって生成される最終コードをアプリケーション初期化の早い段階で利用できる可能性があるため、多くの場合クライアントVMの起動速度より起動が速くなります。また、階層型コンパイルではプロファイリング段階の高速化によってプロファイリングにかけられる時間が長くなるため、通常のサーバーVMよりも優れたピーク・パフォーマンスを実現でき、最適化の向上にもつながる可能性があります。
階層型コンパイルはサーバーVMではデフォルトで有効になっています。64ビット・モードおよび圧縮Ordinary Object Pointerがサポートされています。階層型コンパイルを無効にするには、javaコマンドで-XX:-TieredCompilationフラグを使用します。
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を有効にします。
64ビットJVMプロセスで圧縮Ordinary Object Pointers (OOP)を使用する場合、JVMソフトウェアは、仮想アドレス・ゼロから始まるメモリーをJavaヒープ用に予約するようにオペレーティング・システムに要求を送信します。オペレーティング・システムがこのような要求をサポートしていて、Javaヒープ用のメモリーを仮想アドレス・ゼロで予約できる場合、ゼロ・ベース圧縮OOPが使用されます。
ゼロ・ベース圧縮OOPを使用する場合、Javaヒープ・ベース・アドレスを含めることなく、32ビット・オブジェクト・オフセットから64ビット・ポインタをデコードできます。ヒープ・サイズが4Gバイト未満の場合、JVMソフトウェアはオブジェクト・オフセットの代わりにバイト・オフセットを使用できるため、オフセットを8倍にすることも回避できます。64ビット・アドレスを32ビット・オフセットにエンコードすると、それだけ効率的になります。
Javaヒープ・サイズが26Gバイトまでの場合は、Solaris、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などのクラスのメソッドは、別のスレッドからアクセスできるため同期されています。ただし、ほとんどのシナリオでは、これらはスレッド・ローカルで使用されます。スレッド・ローカルで使用される場合、コンパイラは同期ブロックを最適化して除去することができます。