Byte Code

Revised: Apr./22nd/2004

JVM は二度コンパイルする

郵便配達夫は二度ベルを鳴らし、Java は二度コンパイルするといわれます。

  1. 最初に、ソースコードが、コンパイラ javac によって、バイトコードにコンパイルされます。
  2. 実行時に、バイトコードは、JVM によって、マシンネイティブなオブジェクト・コードにコンパイルされながら実行されます。

バイトコードレベルのマルチプラットフォームは、Java の特徴の一つであり、"Write Once, Run Anywhere." は Java の標語です。

ここで簡単にバイトコードの読み方を紹介します。基本的にはアセンブラであり、javac でコンパイルすることで生成した class ファイルを、javap でディスアセンブルすることで、バイトコードのニーモニックが得られます。

現代的な JVM は、実行環境に応じて、バイトコードに対する処理を動的に変更するため、バイトコードだけを見ても、実行時の動作を一意的に知ることはできません。しかし、ロウ・レベルのステップ数の確認など、バイトコードから得られる知識も馬鹿になりませんので、知っていて損はありません。

Hello, World!

古式ゆかしく "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
フレームが保持するローカル変数配列のインデックス 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;
クラスの static フィールドを取得。今の場合は、PrintStream 型の System.out
ldc #3; //String Hello, World!
実行時コンスタント・プールから項目をプッシュ。今の場合は、文字列リテラル "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
カレントフレームのローカル変数型配列のインデックス 1 の int 型データが、オペランド・スタックにプッシュされる。
bipush 10
byte 値 10 が int 型に変換されてオペランドスタックにプッシュされる。
if_icmpge 74
if_icmp[cmd] は、 int 値がオペランドスタックからポップされて比較される。今の場合は、コマンドが gt (greater than equal) なので、オペランドスタックの value1 ≥ value2 であれば、74 行目に移動し、さもなくば、次の行のインストラクションが実行される。

if 条件の評価

iload_1
カレントフレームのローカル変数型配列のインデックス 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> でオペランド・スタックに intn をプッシュして、if_icmp[cmd] xx でオペランド・スタックから二つの int 値をポップして比較することが必要になるので、0 との比較はステップが一つ省かれていることが分かる。

if 構造の実現

1. even の場合
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 #5; //String even:
実行時コンスタント・プールから項目をプッシュ。今の場合は、文字列リテラル "even" へのシンボル参照をオペランドスタックにプッシュする。
invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
クラスに基づくディスパッチを行い、インスタンス・メソッドを起動する。今の場合は、java/lang/StringBuffer#append に、オペランドスタック上の String 型データを引数を与えて、StringBuffer 型戻り値を受け取っている。
iload_1
カレントフレームのローカル変数型配列のインデックス 1 の int 型データが、オペランド・スタックにプッシュされる。
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
68 行目に移動
2. odd の場合
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:
実行時コンスタント・プールから項目をプッシュ。今の場合は、文字列リテラル "odd" へのシンボル参照をオペランドスタックにプッシュする。
invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
クラスに基づくディスパッチを行い、インスタンス・メソッドを起動する。今の場合は、java/lang/StringBuffer#append に、オペランドスタック上の String 型データを引数を与えて、StringBuffer 型戻り値を受け取っている。
iload_1
カレントフレームのローカル変数型配列のインデックス 1 の int 型データが、オペランド・スタックにプッシュされる。
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
ローカル変数配列のインデックス 1 の int 値に、int 型定数 1 が加算される。
goto 3
3 行目に移動。

メソッドの終了

return
void をリターン。

詳細

本項では、インストラクションごとに、何をやっているのかを紹介しました。一行ずつ追っていくことで、感じがつかめたのではないかと思います。

ここでは、バイトコードの読み方に関する詳細を体系的に説明することはしません。詳細は、「Java仮想マシン仕様書」を参照してください。



Copyright © 2003-2004 SUGAI, Manabu. All Rights Reserved.