Java SE 7 で導入された階層型コンパイルによって、サーバー 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™ 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 対応システムは次のパフォーマンス向上を示しました。