Java SE 7で導入された階層型コンパイルによって、サーバーVMへのクライアント起動が短縮されます。通常、サーバーVMはインタプリタを使用して、コンパイラに渡されるメソッドについてのプロファイリング情報を収集します。階層型方式では、インタプリタに加えてクライアント・コンパイラを使用してコンパイル・バージョンのメソッドが生成され、それらが自身のプロファイリング情報を収集します。コンパイルされたコードはインタプリタよりもはるかに高速なため、プログラムのプロファイリング段階の実行パフォーマンスが大きく向上します。サーバー・コンパイラによって生成される最終コードをアプリケーション初期化の早い段階で利用できる可能性があるため、多くの場合クライアントVMでの起動速度を向上させることができます。また、階層型方式ではプロファイリング段階の高速化によってプロファイリングにかけられる時間が長くなるため、通常のサーバーVMよりも優れたピーク・パフォーマンスを実現でき、最適化の向上にもつながる可能性があります。
階層型コンパイルはサーバーVMのデフォルト・モードです。32ビットと64ビットの両方に加え、圧縮OOPがサポートされます(次のセクションを参照)。階層型コンパイルを無効にするには、java
コマンドで-XX:-TieredCompilation
フラグを使用します。
Java HotSpotの専門用語である「OOP」(Ordinary Object Pointer)は、オブジェクトへの管理ポインタです。OOPは通常、ネイティブ・マシン・ポインタと同じサイズ(LP64システムでは64ビット)です。ILP32システムでは、最大ヒープ・サイズは4Gバイトよりやや少なく、これは多くのアプリケーションにとって十分ではありません。LP64システムでは、指定されたプログラムが使用するヒープが、ILP32システムで実行する場合よりも約1.5倍大きくなることがあります。これほど必要なのは、管理ポインタのサイズが増大することが原因です。メモリーのコストは低いですが、最近では帯域幅およびキャッシュが不足しているため、4Gバイト制限を解決するためだけにヒープ・サイズが大幅に増加するのは望ましくありません。
Javaヒープ内の管理ポインタは、8バイト・アドレス境界に整列されたオブジェクトを指します。圧縮OOPは、(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 6でこの機能を有効にするには、java
コマンドで-XX:+UseCompressedOops
フラグを使用します。
64ビットJava仮想マシン・プロセスで圧縮OOPを使用する場合、JVMソフトウェアは、仮想アドレス・ゼロから始まるメモリーをJavaヒープ用に予約するようにオペレーティング・システムに要求します。オペレーティング・システムがこのような要求をサポートしていて、Javaヒープ用のメモリーを仮想アドレス・ゼロで予約できる場合、ゼロ・ベース圧縮OOPが使用されます。
ゼロ・ベース圧縮OOPを使用することは、Javaヒープ・ベース・アドレス内で加算することなく、64ビット・ポインタを32ビット・オブジェクト・オフセットからデコードできることを意味します。ヒープ・サイズが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
などのクラスのメソッドは、別のスレッドからアクセスできるため同期されています。ただし、ほとんどのシナリオでは、これらはスレッド・ローカルで使用されます。スレッド・ローカルで使用される場合、コンパイラは同期ブロックを最適化して除去することもあります。
Parallel Scavengerガベージ・コレクタは、NUMA (Non Uniform Memory Access)アーキテクチャを持つマシンを利用できるように拡張されています。最新のほとんどのコンピュータは、メモリーの異なる部分にアクセスするためにかかる時間が異なる、NUMAアーキテクチャをベースにしています。通常、システム内の各プロセッサには、アクセス遅延時間が小さく帯域幅の広いローカル・メモリーと、アクセスがはるかに遅いリモート・メモリーが搭載されています。
Java HotSpot仮想マシンでは、NUMA対応アロケータがこのようなシステムを利用するために実装されており、Javaアプリケーションのためにメモリー配置を自動的に最適化します。このアロケータは、若い世代のヒープのEden領域(新しいオブジェクトのほとんどが作成される)を制御します。アロケータはこの領域を、いくつかの領域(それぞれが特定ノードのメモリー内に配置される)に分割します。アロケータは、オブジェクトを割り当てるスレッドがそのオブジェクトをもっとも使用する可能性があるという仮定に依存しています。新しいオブジェクトに最速でアクセスするために、アロケータは割当て側スレッドからローカルな領域内にそれを配置します。領域は、さまざまなノードで実行されているアプリケーション・スレッドの割当て率を反映するために、動的にサイズ変更できます。これにより、単一スレッド・アプリケーションであってもパフォーマンスを向上できます。また、若い世代の「From」および「To」Survivor領域、古い世代、および永続的世代では、ページ・インターリーブが有効になっています。これにより、すべてのスレッドの、これらの領域へのアクセス遅延時間が均一になります。
NUMA対応アロケータは、Solaris(tm) 9 12/02以降のSolarisオペレーティング・システム、およびLinux kernel 2.6.19かつglibc 2.6.1以降のLinuxオペレーティング・システムで利用できます。
NUMA対応アロケータは、-XX:+UseNUMA
フラグとParallel Scavengerガベージ・コレクタの選択によって有効にできます。Parallel Scavengerガベージ・コレクタはサーバークラス・マシンのデフォルトです。Parallel Scavengerガベージ・コレクタは、-XX:+UseParallelGC
オプションを指定することで明示的に有効にすることもできます。
-XX:+UseNUMA
フラグはJava SE 6u2で追加されました。
注: Linuxカーネルには、-XX:UseNUMA
を使用して実行したときにJVMがクラッシュする既知のバグがありました。このバグは2012年に修正されたため、Linuxカーネルの最新バージョンには影響しないはずです。カーネルがこのバグを持つかどうかを確認するために、ネイティブ・リプロデューサを実行できます。
SPEC JBB 2005ベンチマークに基づいて8チップOpteronマシンで評価したときに、NUMA対応システムは次のパフォーマンス向上を示しました。