Javaにおけるコンパイルとは
Java関係だけでもコンパイルという言葉はいろいろな意味で使われています。ここではJavaでよく使われるコンパイルという用語を表1に整理します。ここでFromとToは、これらのコンパイラがそれぞれ何から(From)何へ(To)とコンパイルするかを示しています。
コンパイラの種類 | From | To | 静的/動的 |
---|---|---|---|
Javaコンパイラ | javaソースコード (.javaファイル) |
クラスファイル (.classファイル) |
静的 |
ネイティブコンパイラ | javaソースコード またはバイトコード |
ネイティブコードの実行ファイル | 静的 |
動的コンパイラ(JIT) | メモリ上のjavaバイトコード | メモリ上のネイティブコード | 動的 |
表1 Java環境で使用されるコンパイラの種類 |
ネイティブコンパイラは、高速に動作するネイティブコードの実行ファイルを生成しますが、Javaのメリットでもある動的なクラスのロードに対応できない場合があります。今回の説明の対象としているのは動的コンパイラです。Javaで使われている動的コンパイラは、メモリ上にコードを生成するものであり、ある形式のファイルを生成するものではありません。動的コンパイラは一般にはJIT(Just In Time)コンパイラと呼ばれており、HotSpot VMもこの1つです。
バイトコードの解釈と実行
初期のJava VMはバイトコードインタプリタのみでバイトコードを実行しており、実行速度が非常に遅いという問題がありました。その後、高速化のために動的コンパイルという技術が導入され、現在のようにバイトコードインタプリタと動的コンパイラが併用されるようになりました。HotSpot VMもこの方式で動作します。
図9はHotSpot VMがバイトコードを実行するときの処理の流れです。バイトコードインタプリタと動的コンパイラを組み合わせて動作させます。初めはインタプリタでバイトコードを実行しますが、何度も実行されてパフォーマンスに影響する部分を検出した後にその部分を動的にコンパイルし、次回からはコンパイルされたネイティブコードを実行します。こういったコンパイラは「適応型コンパイラ(adaptive compiler)」と呼ばれます。Javaプログラムが起動後しばらく動作した後に、その処理のほとんどが(1)で示したループで行われる(つまりほとんどの処理が動的コンパイルされたネイティブコードで実行される)ようになれば高速に処理が行われる理想的な状態です。
HotSpot VMでは、Javaスレッドはコンパイル処理を別スレッド(コンパイラスレッド)に実行させます。コンパイラスレッドがコンパイルをしている間、Javaスレッドはコンパイルが完了するのを待たずにインタプリタで処理を続けます。これを図10に示します。動的コンパイルが別スレッドで実行するため、マルチプロセッサの場合には並列処理により効率よく動作します。-Xbatchオプションを指定すれば、Javaスレッドはコンパイラスレッドによりコンパイル処理が終了するのを待ちます。
バイトコードインタプリタが行う処理
Javaバイトコードインタプリタは「バイトコードを1つずつ読み取って解釈し、Java VM内にあるそのバイトコードと等価な処理(そのバイトコードに対応するネイティブコードの固まり)を実行する」という処理を繰り返します。インタプリタで実行されるコードはあらかじめJava VM内に持っているコードで、Javaプログラム実行中に動的に生成するものではありません(正確には、HotSpot VMではJava VM起動時にマクロアセンブラによりインタプリタコードが生成されます)。インタプリタは、概念的にはリスト2のような処理を行います。
while (true) { bytecode = get_next_bytecode (); switch (bytecode.opcode) { case xx: // xxと等価な処理 case yy: // yyと等価な処理 : : } } // xx,yyはオペコード
インタプリタでは、バイトコードを1つずつ解釈するために実行に時間がかかるし、同じ部分が何度繰り返して実行されても実行時間は特別なケースを除いて1回目の実行時間と同じです。ただし、実際の実装では高速化のための工夫がされており、「各バイトコードと等価な処理」も可能な限りベースにあるプロセッサのマシンコードを直接実行するようになっています。-Xintオプションを付けてJava VMを実行すると、動的コンパイルを行わずにインタプリタのみでJavaプログラムを実行させることができます。
インタプリタでは高速化のための工夫がいくつかされています。その1つは、スタックの先頭(Top Of Stack)のレジスタへの割り当てです。インタプリタではスタック上のオペランドスタックを利用しますが、できる限りこのスタックの先頭にレジスタを割り当てています。これにより、メモリアクセスがレジスタアクセスに置き換えられ、インタプリタによるバイトコードの実行がより高速になっています。もう1つの高速化の工夫として、ロードしたバイトコードをインタプリタで2回目に実行する際に、より高速に実行できるバイトコード(fast bytecodeと呼ばれます)へと変換する処理も行っています。
動的コンパイラとは
動的コンパイルの手法にもさまざまな種類がありますが、Javaの動的コンパイラの多くの実装では、「実行中にコンパイルするメソッドを選択し、そのメソッドのバイトコードをネイティブコードに変換し、生成されたコードをメモリ上に格納する」という処理をしています。プログラムの終了時に、動的コンパイルして生成したネイティブコードをファイルに書き出して再利用するといったことは行いません。
初期の動的コンパイラではJava VMがクラスをロードする際にバイトコードをまとめてネイティブコードへと変換していました。また、このころの動的コンパイラは、コードの最適化を行っていませんでした。最適化を行うとさらにコンパイル処理にかかるオーバーヘッドが増えてしまい、トータルの実行時間がかえって長くなってしまうからです。結果としてコンパイルして生成されたコードのサイズが大きいなどの問題がありましたが、コンパイル処理自体は高速でした。動的コンパイラにより一度ネイティブコードへと変換されると、次回からは変換されたコードが直接実行されたため、一部のコードではネイティブと同程度の速度が得られるようになりました。動的コンパイラがJava VMに導入されることにより、Javaプログラムの実行速度は大きく向上し、Javaプログラムのビジネス環境での利用がより現実的になりました。
HotSpot VMでは、プログラム中のメソッドの実行回数が閾(しきい)値を超えると、そのメソッド名が、コンパイル対象のキューに追加され、動的コンパイラによりコンパイルされます。つまりHotSpot VMが、プログラム中で繰り返し実行され、パフォーマンスに影響を与える部分(ホットスポット)を探し出し(図11の(1))、この部分を動的にコンパイルしてネイティブコードへと変換します(図11の(2))。一般に、プログラムのほとんどの処理は一部分のコードで実行されるため、この方法が効果的です。
実際の動的コンパイル処理は、Javaプログラムの実行を行っているスレッドからコンパイラスレッドへと依頼されます。動的コンパイル時には、「バイトコードを中間コード(IR)へと変換し、制御構造を解析し、最適化を行い、ネイティブコードを生成する」というステップを踏みます。これは静的なコンパイラが行う処理に非常に近いものです。
こうして生成されたネイティブコードはコードキャッシュというメモリ領域に置かれ(図11の(3))、次回にこのメソッドが呼び出されたときにはこのネイティブコードが直接実行されます。ただし、HotSpot VMによるコンパイルは決して軽い処理ではありません。実際のH/WやOSなどの環境、またはアプリケーションの特性によっては、オーバーヘッドとなってしまう場合もあります。
コンパイラスレッドは、コンパイル時にほかのメソッドとの関係を調べ、必要であればインライン処理をします。インライン処理とは、呼び出し側のメソッドの中に呼び出される側のメソッドを埋め込む処理で、これによりメソッド呼び出しのオーバーヘッドを減らすことができます。ただしJavaは、クラスをダイナミックローディングする機能を持つオブジェクト指向言語です。そのため、コンパイルしたメソッドとは違うメソッドを実行しなくてはいけないということが、ネイティブコード実行時に判明することがあります。このような場合には安全な場所(safepoint)へネイティブコードの処理を進めてから中断し、その後インタプリタにより処理を継続します(deoptimize)。
動的にコンパイルされたネイティブコードの例
前述したように Java VM は「スタック方式」で動作するスタックマシンですが、動的コンパイルされた部分はRISC、CISC、EPICなどのネイティブコードに変換され、「汎用レジスタ」を利用するコードへと変換されます。汎用レジスタを利用することにより、動的に生成したネイティブコードをより高度にスケジュールして、コードの並列化などの最適化を行うことができます。
ここではEPICアーキテクチャであるItanium2上のHotSpot VMで動的コンパイルして生成されたコードの例を示します。Itanium2には128本もの汎用レジスタがあり、並列度が高く、実行効率の高いコードを生成することができます。リスト3はサンプルのJavaソースコードMyLoop.javaで、このクラスのメソッドmethod1()のバイトコードがリスト4です。
public class TestLoop { static int method1 (int a, int b) { for (int i=0; i<100; i++) System.currentTimeMillis (); return a+b; } public static void main (String [] args) { for (int i=0; i<1000000; i++) { method1 (i, 7); } } }
static int method1(int,int); Code: 0: iconst_0 1: istore_2 2: iload_2 3: bipush 100 5: if_icmpge 18 8: invokestatic #2; //Method java/lang/System.currentTimeMillis:()J 11: pop2 12: iinc 2, 1 15: goto 2 18: iload_0 19: iload_1 20: iadd 21: ireturn
HotSpot VMがリスト4のバイトコードを動的コンパイルして生成した Itaniumのネイティブコードがリスト5です。これを見るとバイトコードが単純にマシンコードにマッピングされているのではなく、スタックベースの処理が、レジスタベースの処理へと変換されているのが分かります。
リスト5では8つの長方形でマシンコードをグループ分けしていますが、このそれぞれを命令グループといいます。ItaniumはEPICアーキテクチャを採用しており、マシンコードレベルで並列実行可能な命令が明示的に示されます。つまり、ここで示されている同じ命令グループ内の命令群は、論理的に同時実行が可能です。Itanium2ではリスト5の各命令グループ内の全命令をプロセッサの1クロックごとに同時発行することができます。
動的コンパイル関連のオプション
HotSpot VMにはインタプリタ/動的コンパイラ関連の以下のオプションがあります。これらを使用することにより、動的コンパイラの動作を設定したり、動的コンパイル実行時の情報を取得したりすることができます。詳細はhttp://java.sun.com/docs/hotspot/VMOptions.htmlを参照してください。
オプション名 | 説明 |
---|---|
-XX:+PrintCompilation | 動的コンパイルしたメソッドを表示する |
-XX:+CITime (1.4のみ) | 動的コンパイルに要した時間などの情報を表示する |
表2 動的コンパイルの情報を表示させることがでるオプション |
-XX:+PrintCompilationオプションを使えば、動的コンパイルが実行されたときに、コンパイルされたメソッド名が表示されます。
$ java -XX:+PrintCompilation MyLoop 1* java.lang.System::currentTimeMillis()J (0 bytes) 2 MyLoop::method1()V (95 bytes) 3 MyLoop::method2(I)V (77 bytes)
JDK 1.4には動的コンパイルの情報を表示する、-XX:+CITimeというオプションがあり、これにより動的コンパイルに関する情報が得られます。
$ java -XX:+CITime MyLoop Accumulated compiler times (for compiled methods only) ------------------------------------------------ Total compilation time : 0.008 s Standard compilation : 0.006 s, Average : 0.003 On stack replacement : 0.000 s, Average : -nan Native methods : 0.003 s, Average : 0.003 Total compiled bytecodes : 172 bytes Standard compilation : 172 bytes On stack replacement : 0 bytes Average compilation speed: 20320 bytes/s nmethod code size : 1360 bytes nmethod total size : 2872 bytes
オプション名/ファイル名 | 説明 |
---|---|
-server | サーバ用VMを起動 |
-client | クライアント用VMを起動 |
-Xint | 動的コンパイルを行わない(インタプリタのみを使用) |
-Xbatch | 動的コンパイルをバックグラウンドで行わない |
-XX:CompileThreshold=10000 | 各メソッドはここで指定された回数だけインタプリタで実行され、この回数を超えると動的コンパイルされる |
-XX:+OnStackReplacement | On stack replacementを有効にする |
.hotspot_compilerファイル | 動的コンパイルしないメソッドを指定できる |
表3 動的コンパイラとインタプリタの動作を指定するオプション |
HotSpot VMは-clientと-serverオプションによりクライアント用VMとサーバ用VMを使い分けることができます。クライアントサイドJavaアプリケーションとサーバサイドJavaアプリケーションでは性質が異なるため、このように2種類のVMが用意されています。クライアント用VMはクライアントサイドJavaプログラム向けに、サーバ用VMはサーバサイドJavaプログラム向けに最適化されています。
クライアントJavaプログラムとはGUIアプリケーションやコマンドなどです。この場合は、起動時間を短くしたい、GUIが停止しないようにしたいという要求から、動的コンパイルと最適化にあまり時間をかけない動作をします。ただし、アプリケーションによっては、-Xintオプションを指定して動的コンパイルを行わずにインタプリタのみで動作させた方が実行時間が短くなるものもあります。
サーバサイドJavaプログラムの代表はJ2EEアプリケーションです。この場合、長時間動作するので、起動時間や動的コンパイルと最適化にかける時間がそれほど問題になりません。つまり、コンパイルや最適化にも多めに時間をかけて実行効率の高いコードを生成することができます。
-XX:+UseOnStackReplacementは、あるメソッド内で繰り返し回数の多いループを実行する場合に効果があります(-XX:+UseCompilerSafePointsも同時に指定する必要があります)。メソッド自体の呼び出し回数が少なくても、そのメソッド内のループの繰り返し回数が多い場合は、繰り返し処理の最中にこのメソッドが動的コンパイルされます。つまり、(1)始めはインタプリタで実行(スタック上にインタプリタ用のフレーム作成)し、(2)途中で繰り返し回数の多いループを検出してメソッド全体を動的コンパイルし、(3)生成されたネイティブコードを実行(インタプリタ用のフレームを無効化してネイティブコード用のフレームを作成)します。下の例では2番目にon stack replacementを有効にしています。loopFunc()というメソッドが動的コンパイルされ、実行時間が約1/300になっているのが分かります。HotSpot VMも環境によってはon stack replacementの機能がデフォルトでONになっているものもあります。この場合は-XX:-UseOnStackReplacementを指定すると明示的にOFFにすることができます。
$ cat OSRLoop.java public class OSRLoop { static void loopFunc () { long time1 = System.currentTimeMillis (); for (int i=0; i<10000000; i++) { /* 適当な処理を入れて下さい */ } long time2 = System.currentTimeMillis (); System.out.println ("elasped time = " + (time2-time1)); } public static void main (String [] args) { loopFunc (); } } $ java -XX:+PrintCompilation OSRLoop elasped time = 7444 $ java -XX:+UseOnStackReplacement -XX:+UseCompilerSafePoints \ -XX:+PrintCompilation OSRLoop 1% OSRLoop::loopFunc()V @ 7 (116 bytes) elasped time = 24
今後のJava VM
Java VMを高速化するための努力はさらに続けられるでしょう。特に実行中のプロファイリング情報を利用したコードの動的な最適化がいろいろな形で行われると予想されます。
後編は、ガベージコレクタの仕組みを解説します。ガベージコレクタはJavaのパフォーマンスをチューニングするための重要なキーワードです。
筆者紹介
日本HP コンサルティング・インテグレーション統括本部
ITコンサルティング本部
テクノロジーコンサルティング部
亀井政利(かめい まさとし)
横河ヒューレット・パッカード(株)(現日本ヒューレット・パッカード(株)に入社後、HP-UXの日本語環境やアプリケーションの開発を担当し、プロジェクトや技術サポートを経験。現在は、主にパートナ向けにJavaを含めたHP-UX全般の技術コンサルティングや、技術情報の発信を中心に活動している。共訳書に「Itanium革命」(ピアソン・エデュケーション)がある。また、日本ヒューレット・パッカードが提供する技術サイト「HP-UX Developer Edge」の執筆を担当している。
Copyright © ITmedia, Inc. All Rights Reserved.