Hotspot VMの基本構造を理解するチューニングのためのJava VM講座(前編)(2/2 ページ)

» 2004年03月11日 00時00分 公開
[亀井政利日本HP]
前のページへ 1|2       

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)で示したループで行われる(つまりほとんどの処理が動的コンパイルされたネイティブコードで実行される)ようになれば高速に処理が行われる理想的な状態です。

図9 インタプリタと動的コンパイラ 図9 インタプリタと動的コンパイラ

 HotSpot VMでは、Javaスレッドはコンパイル処理を別スレッド(コンパイラスレッド)に実行させます。コンパイラスレッドがコンパイルをしている間、Javaスレッドはコンパイルが完了するのを待たずにインタプリタで処理を続けます。これを図10に示します。動的コンパイルが別スレッドで実行するため、マルチプロセッサの場合には並列処理により効率よく動作します。-Xbatchオプションを指定すれば、Javaスレッドはコンパイラスレッドによりコンパイル処理が終了するのを待ちます。

図10 Javaスレッドとコンパイルスレッドの関係 図10 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はオペコード
リスト2 バイトコードインタプリタの処理

 インタプリタでは、バイトコードを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))。一般に、プログラムのほとんどの処理は一部分のコードで実行されるため、この方法が効果的です。

図11 動的コンパイル処理 図11 動的コンパイル処理

 実際の動的コンパイル処理は、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);
    }
  }
}
リスト3 MyLoop.javaソースコード
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
リスト4 MyLoop.javaのmethod1のバイトコード

 HotSpot VMがリスト4のバイトコードを動的コンパイルして生成した Itaniumのネイティブコードがリスト5です。これを見るとバイトコードが単純にマシンコードにマッピングされているのではなく、スタックベースの処理が、レジスタベースの処理へと変換されているのが分かります。

 リスト5では8つの長方形でマシンコードをグループ分けしていますが、このそれぞれを命令グループといいます。ItaniumはEPICアーキテクチャを採用しており、マシンコードレベルで並列実行可能な命令が明示的に示されます。つまり、ここで示されている同じ命令グループ内の命令群は、論理的に同時実行が可能です。Itanium2ではリスト5の各命令グループ内の全命令をプロセッサの1クロックごとに同時発行することができます。

リスト5 動的コンパイルにより生成されたmethod1()のネイティブコード(Itanium2) リスト5 動的コンパイルにより生成されたmethod1()のネイティブコード(Itanium2)

動的コンパイル関連のオプション

 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)
リスト6 -XX:+PrintCompilationオプションの使用例

 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
リスト7 -XX:+CITimeオプションの使用例
オプション名/ファイル名 説明
-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
リスト8 -XX:-UseOnStackReplacementオプションの使用例

今後のJava VM

 Java VMを高速化するための努力はさらに続けられるでしょう。特に実行中のプロファイリング情報を利用したコードの動的な最適化がいろいろな形で行われると予想されます。

 後編は、ガベージコレクタの仕組みを解説します。ガベージコレクタはJavaのパフォーマンスをチューニングするための重要なキーワードです。

筆者紹介

日本HP コンサルティング・インテグレーション統括本部
ITコンサルティング本部
テクノロジーコンサルティング部

亀井政利(かめい まさとし)

横河ヒューレット・パッカード(株)(現日本ヒューレット・パッカード(株)に入社後、HP-UXの日本語環境やアプリケーションの開発を担当し、プロジェクトや技術サポートを経験。現在は、主にパートナ向けにJavaを含めたHP-UX全般の技術コンサルティングや、技術情報の発信を中心に活動している。共訳書に「Itanium革命」(ピアソン・エデュケーション)がある。また、日本ヒューレット・パッカードが提供する技術サイト「HP-UX Developer Edge」の執筆を担当している。



前のページへ 1|2       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。