Revised: Apr./22nd/2004
郵便配達夫は二度ベルを鳴らし、Java は二度コンパイルするといわれます。
バイトコードレベルのマルチプラットフォームは、Java の特徴の一つであり、"Write Once, Run Anywhere." は Java の標語です。
ここで簡単にバイトコードの読み方を紹介します。基本的にはアセンブラであり、javac でコンパイルすることで生成した class ファイルを、javap でディスアセンブルすることで、バイトコードのニーモニックが得られます。
現代的な JVM は、実行環境に応じて、バイトコードに対する処理を動的に変更するため、バイトコードだけを見ても、実行時の動作を一意的に知ることはできません。しかし、ロウ・レベルのステップ数の確認など、バイトコードから得られる知識も馬鹿になりませんので、知っていて損はありません。
古式ゆかしく "Hello, World!" から始めましょう。本項はスタックとヒープについて承知してから読み進めてください。
Hello.java
:
class Hello { public static void main(String[] args) { System.out.println("Hello, World!"); } }
コンパイルとディスアセンブル結果:
C:\java>javac Hello.java C:\java>javap -c Hello Compiled from "Hello.java" class Hello extends java.lang.Object{ Hello(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3; //String Hello, World! 5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }
三つのパートに分かれています。
最初の部分は、ヘッダ部分です。
Compiled from "Hello.java" class Hello extends java.lang.Object{ ... }
ソースコードやクラスの情報が載っています。説明は不要でしょう。クラス Hello
は明示的に継承していないので、全てのクラスが暗黙的に継承する java.lang.Object
を継承するものとしてコンパイルされています。
続いて、ディスアセンブルしたクラスのコンストラクタが載っています。
Hello(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return
クラス Hello
のコンストラクタがどのように実行されるのかが載っています。
aload_0
invokespecial #1; //Method java/lang/Object."<init>":()V
void
。return
void
をリターン。最後に、メソッドになります。非常に短く、この程度だと、何をやっているのかよく分からないかもしれません。
public static void main(java.lang.String[]); Code: 0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3; //String Hello, World! 5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return
getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
PrintStream
型の System.out
。ldc #3; //String Hello, World!
invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
java/io/PrintStream.println
に、オペランドスタック上の String
型データを引数に与えて、void
を受け取っている。return
void
をリターン。全てのバイトコードが表示されるわけではありませんが、インストラクションのニーモニックとコメントにより、何をしているのかが分かります。
より複雑な例として、条件分岐と繰り返しを含む例を挙げます。
class IfDemo { public static void main(String[] args) { for (int i = 9; i < 10; i++) { if (i % 2 == 0) { System.out.println("even: " + i); } else { System.out.println("odd: " + i); } } } }
C:\java>javac IfDemo.java C:\java>javap -c IfDemo Compiled from "IfDemo.java" class IfDemo extends java.lang.Object{ IfDemo(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 9 2: istore_1 3: iload_1 4: bipush 10 6: if_icmpge 74 9: iload_1 10: iconst_2 11: irem 12: ifne 43 15: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 18: new #3; //class StringBuffer 21: dup 22: invokespecial #4; //Method java/lang/StringBuffer."<init>":()V 25: ldc #5; //String even: 27: invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 30: iload_1 31: invokevirtual #7; //Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer; 34: invokevirtual #8; //Method java/lang/StringBuffer.toString:()Ljava/lang/String; 37: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 40: goto 68 43: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 46: new #3; //class StringBuffer 49: dup 50: invokespecial #4; //Method java/lang/StringBuffer."<init>":()V 53: ldc #10; //String odd: 55: invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 58: iload_1 59: invokevirtual #7; //Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer; 62: invokevirtual #8; //Method java/lang/StringBuffer.toString:()Ljava/lang/String; 65: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 68: iinc 1, 1 71: goto 3 74: return } C:\java>
Compiled from "IfDemo.java" class IfDemo extends java.lang.Object{ ... }
IfDemo(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return
結構長くなりました。一行ずつ追っていくと、繰り返し、同じインストラクションが登場していることに気づくでしょう。
このくらい長くなると、バイトコードの意味が分かりやすくなります。
bipush 9
byte
値 9 が int
型に変換されてオペランドスタックにプッシュされる。istore_1
int
値が、カレントフレームのローカル変数型配列のインデックス 1 にセットされる。iload_1
int
型データが、オペランド・スタックにプッシュされる。bipush 10
byte
値 10 が int
型に変換されてオペランドスタックにプッシュされる。if_icmpge 74
if_icmp[cmd]
は、 int
値がオペランドスタックからポップされて比較される。今の場合は、コマンドが gt
(greater than equal) なので、オペランドスタックの value1 ≥ value2 であれば、74 行目に移動し、さもなくば、次の行のインストラクションが実行される。iload_1
int
型データが、オペランド・スタックにプッシュされる。iconst_2
int
定数 2 がオペランドスタックにプッシュされる。irem
int
値をポップして、value1 - (value1/value2)*value2
の結果をオペランドスタックにプッシュする。例えば、value1 = 4, value2 = 2 の場合、4 - (4/2)*2 = 4 - 2*2 = 0
であり、value1 = 5, value2 = 2 の場合、 5 - (5/2)*2 = 5 - (2.5)*2 → 5 - 2*2 = 1
となる。要するに value1 と value2 の剰余である value1 % value2
(value1 mod value2) が計算されており、その結果がオペランド・スタックにプッシュされる。ifne 43
if[cmd]
は、オペランドスタックの int
値をポップして 0 と比較する。今の場合は、コマンドが ne
(not equal) なので、オペランドスタックの value1 ≠ 0 であれば、 43 行目に移動し、さもなくば、次の行のインストラクションが実行される。0 以外との比較は、iconst_<n>
でオペランド・スタックに int
値 n
をプッシュして、if_icmp[cmd] xx
でオペランド・スタックから二つの int
値をポップして比較することが必要になるので、0 との比較はステップが一つ省かれていることが分かる。getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
PrintStream
型の System.out
。new #3; //class StringBuffer
StringBuffer
型オブジェクトがインスタンス化される。dup
invokespecial #4; //Method java/lang/StringBuffer."<init>":()V
StringBuffer
の初期化メソッドが起動。ldc #5; //String even:
invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
java/lang/StringBuffer#append
に、オペランドスタック上の String
型データを引数を与えて、StringBuffer 型戻り値を受け取っている。iload_1
invokevirtual #7; //Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
java/lang/StringBuffer#append
に、オペランドスタック上の int
型データを引数を与えて、StringBuffer 型戻り値を受け取っている。invokevirtual #8; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
java/lang/StringBuffer#toString
に、オペランドスタック上の int
型データを引数を与えて、String 型戻り値を受け取っている。invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
java/io/PrintStream#println
に、オペランドスタック上の String
型データを引数を与えて、void 型戻り値を受け取っている。goto 68
getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
static
フィールドが取得されて、オペランドスタックにプッシュされる。今の場合は、PrintStream
型の System.out
。
new #3; //class StringBuffer
StringBuffer
型オブジェクトがインスタンス化される。dup
invokespecial #4; //Method java/lang/StringBuffer."<init>":()V
StringBuffer
の初期化メソッドが起動。ldc #10; //String odd:
invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
java/lang/StringBuffer#append
に、オペランドスタック上の String
型データを引数を与えて、StringBuffer 型戻り値を受け取っている。iload_1
invokevirtual #7; //Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
java/lang/StringBuffer#append
に、オペランドスタック上の int
型データを引数を与えて、StringBuffer 型戻り値を受け取っている。invokevirtual #8; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
java/lang/StringBuffer#toString
に、オペランドスタック上の int
型データを引数を与えて、String 型戻り値を受け取っている。invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
java/io/PrintStream#println
に、オペランドスタック上の String
型データを引数を与えて、void 型戻り値を受け取っている。iinc 1, 1
int
値に、int
型定数 1
が加算される。goto 3
return
void
をリターン。本項では、インストラクションごとに、何をやっているのかを紹介しました。一行ずつ追っていくことで、感じがつかめたのではないかと思います。
ここでは、バイトコードの読み方に関する詳細を体系的に説明することはしません。詳細は、「Java仮想マシン仕様書」を参照してください。