javacしたときに生成されるclassファイルを解読してみる

前置き

Javaファイルをコンパイルしたときに生成されるclassファイル。

これがJVMでどのように実行されているかに興味を持ったので調べてみた備忘録。

動作環境

Shogo-no-MacBook-Pro$ java -version                                                                                                
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)

ステップ1

public class Calculator{
  public static int return1(){
     return 1;
   }
}

まずは単にint型の1を返却する静的メソッドを持つCalculatorクラスを作成した。 早速、ソースコードコンパイルする。

javac Calculator.java

コンパイルすると、classファイルが生成される。 javapコマンドを実行してみよう。

javap -v Calculator

すると、以下が標準出力に表示されるはずだ。

Classfile /Users/Shogo/Java_src/Test/Calculator.class
  Last modified 2018/07/17; size 250 bytes
  MD5 checksum 16f085b520f032ca19fbaf234c3ce4f4
  Compiled from "Calculator.java"
public class Calculator
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // Calculator
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               return1
   #9 = Utf8               ()I
  #10 = Utf8               SourceFile
  #11 = Utf8               Calculator.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               Calculator
  #14 = Utf8               java/lang/Object
{
  public Calculator();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static int return1();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: ireturn
      LineNumberTable:
        line 3: 0
}
SourceFile: "Calculator.java"

javapコマンドは、classファイルを可視化するコマンドである。 数多くの情報が出力されているが、今回はreturn1メソッドの部分だけに着目して読み解いてみよう。

  public static int return1();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: ireturn
      LineNumberTable:
        line 3: 0
}
SourceFile: "Calculator.java"

Javaの実行環境であるJVM(Java Virtual Machine)は、スタックマシン上でコードが実行していく。classファイルでもスタックマシンで動作するコードが確認することができる。

  1. iconst_1 定数1をスタックに詰める命令である。 スタックマシンでは、returnする値をスタックに詰める必要がある。

  2. ireturn スタックのトップを呼び出し元に返却するための命令である。 ここでは、先ほど詰めた定数1をそのまま返却する。 ちなみに、先頭に「i」がついているのはint型のことを示している。

ステップ2

次は、引数を取る返却するメソッドretrunAを作成してみる。

public class Calculator{
  public static int returnA(int a){
    return a;
  }
}

そして、javapした結果が以下である。

  public static int returnA(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: ireturn
      LineNumberTable:
        line 7: 0
}
SourceFile: "Calculator.java"

0.iload_0 ステップ1の流れから、returnする値をスタックに詰める必要がある。 つまり、ここでは引数をロードして、スタックに詰める命令に該当する。

  1. ireturn ステップ1と同様なので割愛。

ステップ3

それでは次は、引数に1を加算して返却するメソッドを作ってみよう。

public class Calculator{
  public static int plus1(int a){
    return a + 1;
  }  
}
  public static int plus1(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iload_0
         1: iconst_1
         2: iadd
         3: ireturn
      LineNumberTable:
        line 11: 0

さて、これまで見てきた二つのメソッドを合体させたような命令となっている。 plus1メソッドでは以下が実行される。

  1. iload_0 引数aをスタックにpushする

  2. iconst_1 定数1をスタックにpushする

  3. iadd 新しく登場した命令だが、int型をaddする命令であることが容易に想像できる。 スタックに積まれている二つの値(aと1)を加算し、スタックにpushする命令に当たる。

  4. ireturn 値を返却する。

ステップ4

さて、ステップ3のインスタンスメソッドを静的メソッドに変更してみるとどうなるだろうか?

public class Calculator{
  // staticを取ってみる
  public int plus1(int a){
    return a + 1;
  }  
}
  public int plus1(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_1
         1: iconst_1
         2: iadd
         3: ireturn
      LineNumberTable:
        line 11: 0
  1. iload1 ステップ3のケースと比較すると、iload_0がiload_1になっていることがわかる。 実はインスタンスメソッドの場合、iload_0ではthisを指すことになっているためである。(ステップ5で実際に見てみよう) つまり一番目の引数はiload_1に該当する。

  2. iconst_1

  3. iadd
  4. ireturn

これまでと同様なので割愛。

ステップ5

thisのインスタンスを指す場合はiload_0を指すとのことなので、早速確認してみよう。

public class Calculator{
  private int number = 1;

  public int plus1(){
    return this.number + 1;
  }
}

フィールドとしてnumber変数を導入し、 plus1メソッドではそのフィールドに1を加算するように修正した。

  public int plus1();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field number:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 5: 0
}
SourceFile: "Calculator.java"
  1. aload_0 thisインスタンスをスタックに詰める命令である。

  2. getfield #2 スタックに詰まっているthisインスタンスの属性値を取得し、スタックに詰める命令である。 フィールドであるnumberの値を取得し、スタックに詰めている。

  3. iconst_1

  4. iadd
  5. ireturn

これまでと同様のため、割愛する。

関連リンク

The Java® Virtual Machine Specification

続きはまた書きます