1|2JavaにおけるコンパイルとはJava関係だけでもコンパイルという言葉はいろいろな意味で使われています。ここではJavaでよく使われるコンパイルという用語を表1に整理します。ここでFromとToは、これらのコンパイラがそれぞれ何から(From)何へ(To)とコンパイルするかを示しています。Java環境で使用されるコンパイラの種類ネイティブコンパイラは、高速に動作するネイティブコードの実行ファイルを生成しますが、Javaのメリットでもある動的なクラスのロードに対応できない場合があります。今回の説明の対象としているのは動的コンパイラです。Javaで使われている動的コンパイラは、メモリ上にコードを生成するものであり、ある形式のファイルを生成するものではありません。VMはバイトコードインタプリタのみでバイトコードを実行しており、実行速度が非常に遅いという問題がありました。その後、高速化のために動的コンパイルという技術が導入され、現在のようにバイトコードインタプリタと動的コンパイラが併用されるようになりました。VMもこの方式で動作します。VMがバイトコードを実行するときの処理の流れです。バイトコードインタプリタと動的コンパイラを組み合わせて動作させます。初めはインタプリタでバイトコードを実行しますが、何度も実行されてパフォーマンスに影響する部分を検出した後にその部分を動的にコンパイルし、次回からはコンパイルされたネイティブコードを実行します。compiler)」と呼ばれます。Javaプログラムが起動後しばらく動作した後に、その処理のほとんどが(1)で示したループで行われる(つまりほとんどの処理が動的コンパイルされたネイティブコードで実行される)ようになれば高速に処理が行われる理想的な状態です。VMでは、Javaスレッドはコンパイル処理を別スレッド(コンパイラスレッド)に実行させます。コンパイラスレッドがコンパイルをしている間、Javaスレッドはコンパイルが完了するのを待たずにインタプリタで処理を続けます。これを図10に示します。動的コンパイルが別スレッドで実行するため、マルチプロセッサの場合には並列処理により効率よく動作します。-Xbatchオプションを指定すれば、Javaスレッドはコンパイラスレッドによりコンパイル処理が終了するのを待ちます。VM内にあるそのバイトコードと等価な処理(そのバイトコードに対応するネイティブコードの固まり)を実行する」という処理を繰り返します。VM起動時にマクロアセンブラによりインタプリタコードが生成されます)。インタプリタは、概念的にはリスト2のような処理を行います。バイトコードインタプリタの処理インタプリタでは、バイトコードを1つずつ解釈するために実行に時間がかかるし、同じ部分が何度繰り返して実行されても実行時間は特別なケースを除いて1回目の実行時間と同じです。実際の実装では高速化のための工夫がされており、「各バイトコードと等価な処理」も可能な限りベースにあるプロセッサのマシンコードを直接実行するようになっています。VMを実行すると、動的コンパイルを行わずにインタプリタのみでJavaプログラムを実行させることができます。インタプリタでは高速化のための工夫がいくつかされています。Stack)のレジスタへの割り当てです。インタプリタではスタック上のオペランドスタックを利用しますが、できる限りこのスタックの先頭にレジスタを割り当てています。これにより、メモリアクセスがレジスタアクセスに置き換えられ、インタプリタによるバイトコードの実行がより高速になっています。bytecodeと呼ばれます)へと変換する処理も行っています。動的コンパイラとは動的コンパイルの手法にもさまざまな種類がありますが、Javaの動的コンパイラの多くの実装では、「実行中にコンパイルするメソッドを選択し、そのメソッドのバイトコードをネイティブコードに変換し、生成されたコードをメモリ上に格納する」という処理をしています。プログラムの終了時に、動的コンパイルして生成したネイティブコードをファイルに書き出して再利用するといったことは行いません。VMがクラスをロードする際にバイトコードをまとめてネイティブコードへと変換していました。このころの動的コンパイラは、コードの最適化を行っていませんでした。最適化を行うとさらにコンパイル処理にかかるオーバーヘッドが増えてしまい、トータルの実行時間がかえって長くなってしまうからです。結果としてコンパイルして生成されたコードのサイズが大きいなどの問題がありましたが、コンパイル処理自体は高速でした。動的コンパイラにより一度ネイティブコードへと変換されると、次回からは変換されたコードが直接実行されたため、一部のコードではネイティブと同程度の速度が得られるようになりました。VMに導入されることにより、Javaプログラムの実行速度は大きく向上し、Javaプログラムのビジネス環境での利用がより現実的になりました。VMでは、プログラム中のメソッドの実行回数が閾(しきい)値を超えると、そのメソッド名が、コンパイル対象のキューに追加され、動的コンパイラによりコンパイルされます。VMが、プログラム中で繰り返し実行され、パフォーマンスに影響を与える部分(ホットスポット)を探し出し(図11の(1))、この部分を動的にコンパイルしてネイティブコードへと変換します(図11の(2))。一般に、プログラムのほとんどの処理は一部分のコードで実行されるため、この方法が効果的です。実際の動的コンパイル処理は、Javaプログラムの実行を行っているスレッドからコンパイラスレッドへと依頼されます。動的コンパイル時には、「バイトコードを中間コード(IR)へと変換し、制御構造を解析し、最適化を行い、ネイティブコードを生成する」というステップを踏みます。これは静的なコンパイラが行う処理に非常に近いものです。して生成されたネイティブコードはコードキャッシュというメモリ領域に置かれ(図11の(3))、次回にこのメソッドが呼び出されたときにはこのネイティブコードが直接実行されます。VMによるコンパイルは決して軽い処理ではありません。実際のH/WやOSなどの環境、またはアプリケーションの特性によっては、オーバーヘッドとなってしまう場合もあります。コンパイラスレッドは、コンパイル時にほかのメソッドとの関係を調べ、必要であればインライン処理をします。インライン処理とは、呼び出し側のメソッドの中に呼び出される側のメソッドを埋め込む処理で、これによりメソッド呼び出しのオーバーヘッドを減らすことができます。Javaは、クラスをダイナミックローディングする機能を持つオブジェクト指向言語です。そのため、コンパイルしたメソッドとは違うメソッドを実行しなくてはいけないということが、ネイティブコード実行時に判明することがあります。このような場合には安全な場所(safepoint)へネイティブコードの処理を進めてから中断し、その後インタプリタにより処理を継続します(deoptimize)。「スタック方式」で動作するスタックマシンですが、動的コンパイルされた部分はRISC、CISC、EPICなどのネイティブコードに変換され、「汎用レジスタ」を利用するコードへと変換されます。汎用レジスタを利用することにより、動的に生成したネイティブコードをより高度にスケジュールして、コードの並列化などの最適化を行うことができます。VMで動的コンパイルして生成されたコードの例を示します。Itanium2には128本もの汎用レジスタがあり、並列度が高く、実行効率の高いコードを生成することができます。リスト3はサンプルのJavaソースコードMyLoop.javaで、このクラスのメソッドmethod1()のバイトコードがリスト4です。Itaniumのネイティブコードがリスト5です。これを見るとバイトコードが単純にマシンコードにマッピングされているのではなく、スタックベースの処理が、レジスタベースの処理へと変換されているのが分かります。リスト5では8つの長方形でマシンコードをグループ分けしていますが、このそれぞれを命令グループといいます。ItaniumはEPICアーキテクチャを採用しており、マシンコードレベルで並列実行可能な命令が明示的に示されます。ここで示されている同じ命令グループ内の命令群は、論理的に同時実行が可能です。Itanium2ではリスト5の各命令グループ内の全命令をプロセッサの1クロックごとに同時発行することができます。VMにはインタプリタ/動的コンパイラ関連の以下のオプションがあります。これらを使用することにより、動的コンパイラの動作を設定したり、動的コンパイル実行時の情報を取得したりすることができます。詳細はhttp://java.sun.com/docs/hotspot/VMOptions.htmlを参照してください。動的コンパイルの情報を表示させることがでるオプション-XX:+PrintCompilationオプションを使えば、動的コンパイルが実行されたときに、コンパイルされたメソッド名が表示されます。1.4には動的コンパイルの情報を表示する、-XX:+CITimeというオプションがあり、これにより動的コンパイルに関する情報が得られます。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)生成されたネイティブコードを実行(インタプリタ用のフレームを無効化してネイティブコード用のフレームを作成)します。replacementを有効にしています。loopFunc()というメソッドが動的コンパイルされ、実行時間が約1/300になっているのが分かります。replacementの機能がデフォルトでONになっているものもあります。この場合は-XX:-UseOnStackReplacementを指定すると明示的にOFFにすることができます。VMを高速化するための努力はさらに続けられるでしょう。実行中のプロファイリング情報を利用したコードの動的な最適化がいろいろな形で行われると予想されます。後編は、ガベージコレクタの仕組みを解説します。ガベージコレクタはJavaのパフォーマンスをチューニングするための重要なキーワードです。まさとし)横河ヒューレット・パッカード(株)(現日本ヒューレット・パッカード(株)に入社後、HP-UXの日本語環境やアプリケーションの開発を担当し、プロジェクトや技術サポートを経験。現在は、主にパートナ向けにJavaを含めたHP-UX全般の技術コンサルティングや、技術情報の発信を中心に活動している。共訳書に「Itanium革命」(ピアソン・エデュケーション)がある。Edge」の執筆を担当している。メールマガジンをご購読ください。申し込みページへITmediaはアイティメディア株式会社の登録商標です。