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_0invokespecial #1; //Method java/lang/Object."<init>":()Vvoid。returnvoid をリターン。最後に、メソッドになります。非常に短く、この程度だと、何をやっているのかよく分からないかもしれません。
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;)Vjava/io/PrintStream.println に、オペランドスタック上の String 型データを引数に与えて、void を受け取っている。returnvoid をリターン。全てのバイトコードが表示されるわけではありませんが、インストラクションのニーモニックとコメントにより、何をしているのかが分かります。
より複雑な例として、条件分岐と繰り返しを含む例を挙げます。
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 9byte 値 9 が int 型に変換されてオペランドスタックにプッシュされる。istore_1int 値が、カレントフレームのローカル変数型配列のインデックス 1 にセットされる。iload_1int 型データが、オペランド・スタックにプッシュされる。bipush 10byte 値 10 が int 型に変換されてオペランドスタックにプッシュされる。if_icmpge 74if_icmp[cmd] は、 int 値がオペランドスタックからポップされて比較される。今の場合は、コマンドが gt (greater than equal) なので、オペランドスタックの value1 ≥ value2 であれば、74 行目に移動し、さもなくば、次の行のインストラクションが実行される。iload_1int 型データが、オペランド・スタックにプッシュされる。iconst_2int 定数 2 がオペランドスタックにプッシュされる。iremint 値をポップして、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 43if[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 StringBufferStringBuffer 型オブジェクトがインスタンス化される。dupinvokespecial #4; //Method java/lang/StringBuffer."<init>":()VStringBuffer の初期化メソッドが起動。ldc #5; //String even:invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;java/lang/StringBuffer#append に、オペランドスタック上の String 型データを引数を与えて、StringBuffer 型戻り値を受け取っている。iload_1invokevirtual #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;)Vjava/io/PrintStream#println に、オペランドスタック上の String 型データを引数を与えて、void 型戻り値を受け取っている。goto 68getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;static フィールドが取得されて、オペランドスタックにプッシュされる。今の場合は、PrintStream 型の System.out。
new #3; //class StringBufferStringBuffer 型オブジェクトがインスタンス化される。dupinvokespecial #4; //Method java/lang/StringBuffer."<init>":()VStringBuffer の初期化メソッドが起動。ldc #10; //String odd:invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;java/lang/StringBuffer#append に、オペランドスタック上の String 型データを引数を与えて、StringBuffer 型戻り値を受け取っている。iload_1invokevirtual #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;)Vjava/io/PrintStream#println に、オペランドスタック上の String 型データを引数を与えて、void 型戻り値を受け取っている。iinc 1, 1int 値に、int 型定数 1 が加算される。goto 3returnvoid をリターン。本項では、インストラクションごとに、何をやっているのかを紹介しました。一行ずつ追っていくことで、感じがつかめたのではないかと思います。
ここでは、バイトコードの読み方に関する詳細を体系的に説明することはしません。詳細は、「Java仮想マシン仕様書」を参照してください。