本連載の最終回は、HotSpot VMに固有の振る舞いを学びます。HotSpot VMのデフォルト設定ではパフォーマンスが思うように向上しないケースを紹介し、対処方法を説明します。また後半では、JVMのPermanent領域のチューニング方法を説明します。
アプリケーションのチューニングの効果を比較するためにしばしば作成されるのが、簡単なベンチマーク・プログラムです。しかし、HotSpotベースのJVMでは、ベンチマークの設計のまずさが誤解を生む原因となりかねません。例えば、ベンチマークによっては、旧型のJIT(Just-In-Time)コンパイラを備えたClassic JVMの方が高速になることもあるのです。
まずは、以下のベンチマーク・プログラムをご覧ください。
public class SimpleBenchmark { public static void main(String[] argv) { int value=0; // Record the start time. long start= System.currentTimeMillis(); // Repeatedly executes feature to measure performance. for (int i=0; i<100000000; i++) { // Replace line with your favorite computation. value +=i; } // Record the finish time. long finish= System.currentTimeMillis(); // Now report how long test ran. System.out.print ("Time spent = " + Long.toString(finish - start) + " ms\n"); } }
ご覧のとおり、ループを1億回繰り返し、その経過時間を測るという簡単なベンチマークです。このプログラムをコンパイルし、デフォルトのHotSpot JVMで実行すると、以下のような結果が得られます。
$ java SimpleBenchmark Time spent = 24168 ms
一方、JDK 1.3以前のJVMの起動オプションに-classicを指定し、Classic JVMで実行してみます(なお同オプションはJDK 1.4以降ではサポートされていません)。
$ java -classic SimpleBenchmark Time spent = 911 ms
このように、HotSpot VMはClassic JVMよりも25倍も遅いという結果が得られます。
ここで、JITコンパイルを一切行わず、すべてをインタプリタ実行するとどのような結果が得られるか試してみましょう。JVMの起動時に-Xintオプションを指定することで、純粋なインタプリタとして動作させることができます。その結果は以下のとおりです。
$ java -Xint SimpleBenchmark Time spent = 24100 ms
このように、上述したHotSpot VMによる結果は、インタプリタ実行時の結果とほぼ同じであることが分かります。
では、なぜこのような違いが表れるのでしょうか。その理由を探るために、HotSpot VMとClassic JVMの差を比較してみます。
JVMの種類 | 特長 |
---|---|
Classic JVM | JIT(Just-In-Time)コンパイラを搭載する。実行されるすべてのコードをコンパイルする |
HotSpot VM | HotSpotコンパイラを搭載する。アプリケーションにおいて最も実行頻度の高い部分のみコンパイルする |
一般的なプログラムには、コードの20%の部分に実行時間の80%が費やされるという、いわゆる80/20の法則が当てはまります。HotSpot VMは、この80/20の法則に基づいて設計されています。つまり、同JVMでは最初はアプリケーションをインタプリタ・モードで実行し、コードの実行頻度の解析を行います。これにより、「HotSpot」すなわち実行頻度の高い部分を特定したならば、その部分についてのみバイナリ・コードへのコンパイルやインライン展開などの最適化を実施します。これに対し、Classic JVMでは、実行されるすべてのコードをコンパイルするため、コンパイル時間がオーバーヘッドとなります。
本来であれば、HotSpot VMの方がClassic JVMよりも優れたパフォーマンスを実現できるはずです。しかし上述したベンチマークの例では、ループを含むメソッドが1回しか呼び出されないため、インタプリタ・モードのままループを実行してしまうのです。
こうした場合は、JVMの起動オプションとして-XX:+UseOnStackReplacementを指定し、On Stack Replacement機能を有効にします(-XX:+UseCompilerSafepointsを同時に指定する必要があります*1)。このオプションを利用すると、メソッド呼び出しの最中であっても、インタプリタ実行からコンパイル済みバイナリの実行へ切り替えられるようになります。これにより、以下のような結果が得られます。
$ java -XX:+UseCompilerSafepoints -XX:+UseOnStackReplacement SimpleBenchmark Time spent = 42 ms
このように、Classic JVMより優れた結果が得られることがわかります。
上記の例のように、比較的短い時間で終了するベンチマークは「マイクロ・ベンチマーク」と呼ばれます。しかし上述したとおり、HotSpot VMでは最適化を始める前にインタプリタ・モードでコードを実行し、HotSpotの分析に時間を費やします。そのため、マイクロ・ベンチマークでは、HotSpot VMによる最適化の効果をほとんど得ることができません。
HotSpot VMにおいて、こうしたマイクロ・ベンチマークで十分なパフォーマンスを得るには、ベンチマーク部分をメソッドとして切り出し、それを繰り返し呼び出すようにコードを修正します。以下は、上述のベンチマークを修正した例です。
public class HotSpotBenchmark { public static void runTest() { int value=0; // Repeatedly executes feature to measure performance. for (int i=0; i<100000000; i++) { // Replace line with your favorite computation. value +=i; } } public static void main(String[] argv) { // Run benchmark multiple times. This will allow us to // see when HotSpot begins executing compiled code. for (int i = 0; i < 8; i++) { // Record the start time. long start= System.currentTimeMillis(); // Run benchmark test. runTest(); // Record the finish time. long finish= System.currentTimeMillis(); // Now report how long test ran. System.out.print ("Time spent = " + Long.toString(finish - start) + " ms\n"); } } }
このプログラムをHotSpot VMで実行すると、以下のような結果が得られます。
$ java HotSpotBenchmark Time spent = 23372 ms Time spent = 23400 ms Time spent = 23372 ms Time spent = 11 ms Time spent = 11 ms Time spent = 11 ms
ここで、ベンチマーク部分のメソッドはインタプリタ・モードのままで3回実行されていることに注目してください。一般には、HotSpot VMが最適化を終えるまでには1〜2分を要します。その最適化の結果、HotSpot VMではClassic JVMの80倍の高速化を達成していることが分かります。
パフォーマンス・チューニングを目的としてベンチマークを作成する際には、それがアプリケーション全体のアーキテクチャを反映できているかを見直してください。HotSpot VMの効果を高めるには、そのベンチマークが十分に長時間実行され、「HotSpot」部分が繰り返し呼び出されなくてはならないのです。
Copyright © ITmedia, Inc. All Rights Reserved.