mike-neckのブログ

Java or Groovy or Swift or Golang

JJUG ナイトセミナー 「Javaのプログラムはどうやって動いているの」で正解者が少なかったあの問題をバイトコードから見てみる #jjug

昨日のJJUG ナイトセミナーで出題されて正解者が少なかったAnniversary.javaの問題をjavapして、どのようにコードが実行されているか見てみようというだけのエントリーです。

なお、

だそうです。


問題文

以下の問題を実行した時に標準出力に出力されるのはどれ?

import java.time.*;

public class Anniversary {
    private static final Anniversary ANN = new Anniversary();
    private static final int BIRTH = Year.of(1995).getValue();
    private static final int NOW = Year.now().getValue();

    private final int age;

    public int getAge() {
        return age;
    }

    public Anniversary() {
        this.age = NOW - BIRTH;
    }

    public static void main(String... args) {
        System.out.println("the age is " + ANN.getAge() + ".");
    }
}

選択肢

  1. the age is 0.
  2. the age is 20.
  3. the age is 1995.
  4. 例外が発生

javapしてみる

上記のクイズの正解は1.です。

では、バイトコードがどのように実行されていくか、一つずつ見て行きたいと思います。

上記のコードをコンパイルしてjavap -c -vした結果は次のようになります。

Classfile /path/to/Anniversary.class
  Last modified 2015/04/25; size 1061 bytes
  MD5 checksum 11bad503406cc2ec015ddc95d15a520c
  Compiled from "Anniversary.java"
public class Anniversary
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #16.#39        // Anniversary.age:I
   #2 = Methodref          #21.#40        // java/lang/Object."<init>":()V
   #3 = Fieldref           #16.#41        // Anniversary.NOW:I
   #4 = Fieldref           #16.#42        // Anniversary.BIRTH:I
   #5 = Fieldref           #43.#44        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Class              #45            // java/lang/StringBuilder
   #7 = Methodref          #6.#40         // java/lang/StringBuilder."<init>":()V
   #8 = String             #46            // the age is
   #9 = Methodref          #6.#47         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #10 = Fieldref           #16.#48        // Anniversary.ANN:LAnniversary;
  #11 = Methodref          #16.#49        // Anniversary.getAge:()I
  #12 = Methodref          #6.#50         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #13 = String             #51            // .
  #14 = Methodref          #6.#52         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #15 = Methodref          #53.#54        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #16 = Class              #55            // Anniversary
  #17 = Methodref          #16.#40        // Anniversary."<init>":()V
  #18 = Methodref          #56.#57        // java/time/Year.of:(I)Ljava/time/Year;
  #19 = Methodref          #56.#58        // java/time/Year.getValue:()I
  #20 = Methodref          #56.#59        // java/time/Year.now:()Ljava/time/Year;
  #21 = Class              #60            // java/lang/Object
  #22 = Utf8               ANN
  #23 = Utf8               LAnniversary;
  #24 = Utf8               BIRTH
  #25 = Utf8               I
  #26 = Utf8               NOW
  #27 = Utf8               age
  #28 = Utf8               getAge
  #29 = Utf8               ()I
  #30 = Utf8               Code
  #31 = Utf8               LineNumberTable
  #32 = Utf8               <init>
  #33 = Utf8               ()V
  #34 = Utf8               main
  #35 = Utf8               ([Ljava/lang/String;)V
  #36 = Utf8               <clinit>
  #37 = Utf8               SourceFile
  #38 = Utf8               Anniversary.java
  #39 = NameAndType        #27:#25        // age:I
  #40 = NameAndType        #32:#33        // "<init>":()V
  #41 = NameAndType        #26:#25        // NOW:I
  #42 = NameAndType        #24:#25        // BIRTH:I
  #43 = Class              #61            // java/lang/System
  #44 = NameAndType        #62:#63        // out:Ljava/io/PrintStream;
  #45 = Utf8               java/lang/StringBuilder
  #46 = Utf8               the age is
  #47 = NameAndType        #64:#65        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #48 = NameAndType        #22:#23        // ANN:LAnniversary;
  #49 = NameAndType        #28:#29        // getAge:()I
  #50 = NameAndType        #64:#66        // append:(I)Ljava/lang/StringBuilder;
  #51 = Utf8               .
  #52 = NameAndType        #67:#68        // toString:()Ljava/lang/String;
  #53 = Class              #69            // java/io/PrintStream
  #54 = NameAndType        #70:#71        // println:(Ljava/lang/String;)V
  #55 = Utf8               Anniversary
  #56 = Class              #72            // java/time/Year
  #57 = NameAndType        #73:#74        // of:(I)Ljava/time/Year;
  #58 = NameAndType        #75:#29        // getValue:()I
  #59 = NameAndType        #76:#77        // now:()Ljava/time/Year;
  #60 = Utf8               java/lang/Object
  #61 = Utf8               java/lang/System
  #62 = Utf8               out
  #63 = Utf8               Ljava/io/PrintStream;
  #64 = Utf8               append
  #65 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #66 = Utf8               (I)Ljava/lang/StringBuilder;
  #67 = Utf8               toString
  #68 = Utf8               ()Ljava/lang/String;
  #69 = Utf8               java/io/PrintStream
  #70 = Utf8               println
  #71 = Utf8               (Ljava/lang/String;)V
  #72 = Utf8               java/time/Year
  #73 = Utf8               of
  #74 = Utf8               (I)Ljava/time/Year;
  #75 = Utf8               getValue
  #76 = Utf8               now
  #77 = Utf8               ()Ljava/time/Year;
{
  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 11: 0

  public Anniversary();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: getstatic     #3                  // Field NOW:I
         8: getstatic     #4                  // Field BIRTH:I
        11: isub
        12: putfield      #1                  // Field age:I
        15: return
      LineNumberTable:
        line 14: 0
        line 15: 4
        line 16: 15

  public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #6                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #8                  // String the age is
        12: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: getstatic     #10                 // Field ANN:LAnniversary;
        18: invokevirtual #11                 // Method getAge:()I
        21: invokevirtual #12                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        24: ldc           #13                 // String .
        26: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        29: invokevirtual #14                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        32: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        35: return
      LineNumberTable:
        line 19: 0
        line 20: 35

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #16                 // class Anniversary
         3: dup
         4: invokespecial #17                 // Method "<init>":()V
         7: putstatic     #10                 // Field ANN:LAnniversary;
        10: sipush        1995
        13: invokestatic  #18                 // Method java/time/Year.of:(I)Ljava/time/Year;
        16: invokevirtual #19                 // Method java/time/Year.getValue:()I
        19: putstatic     #4                  // Field BIRTH:I
        22: invokestatic  #20                 // Method java/time/Year.now:()Ljava/time/Year;
        25: invokevirtual #19                 // Method java/time/Year.getValue:()I
        28: putstatic     #3                  // Field NOW:I
        31: return
      LineNumberTable:
        line 4: 0
        line 5: 10
        line 6: 22
}
SourceFile: "Anniversary.java"

staticイニシャライザー

まずはstaticイニシャライザーの部分から。

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #16                 // class Anniversary
         3: dup
         4: invokespecial #17                 // Method "<init>":()V
         7: putstatic     #10                 // Field ANN:LAnniversary;
        10: sipush        1995
        13: invokestatic  #18                 // Method java/time/Year.of:(I)Ljava/time/Year;
        16: invokevirtual #19                 // Method java/time/Year.getValue:()I
        19: putstatic     #4                  // Field BIRTH:I
        22: invokestatic  #20                 // Method java/time/Year.now:()Ljava/time/Year;
        25: invokevirtual #19                 // Method java/time/Year.getValue:()I
        28: putstatic     #3                  // Field NOW:I
        31: return
      LineNumberTable:
        line 4: 0
        line 5: 10
        line 6: 22

(1) 0: new #16

  • テーブルの#16(class Anniversary)に対して、new命令をします。
    • 操作内容 - 新たなオブジェクトを生成する
    • スタックの状態
      • [] =>
      • [objectref(Anniversary)]

(2) 3: dup

  • dup命令を実行します。
    • 操作内容 - オペランドスタックの先頭にある値を複製します。
    • スタックの状態
      • [objectref(Anniversary)] =>
      • [objectref(Anniversary), objectref(Anniversary)]

(3) 4: invokespecial #17

ここで、処理はコンストラクターに移動します。

  public Anniversary();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: getstatic     #3                  // Field NOW:I
         8: getstatic     #4                  // Field BIRTH:I
        11: isub
        12: putfield      #1                  // Field age:I
        15: return
      LineNumberTable:
        line 14: 0
        line 15: 4
        line 16: 15

(4) 0: aload_0

  • aload_0命令を実行します。
    • 操作内容 - ローカル変数からreferenceをロードします。
    • スタックの状態
      • [] =>
      • [objectref(this)]

(5) 1: invokespecial #2

  • invokespecial命令(既出)を実行します。

(6) 4: aload_0

  • aload_0命令(既出)を実行します。
    • 操作内容 - ローカル変数からreferenceをロードします。
    • スタックの状態
      • [] =>
      • [objectref(this)]

(7) 5: getstatic #3

  • getstatic命令を実行します。
    • 操作内容 - クラスのstaticフィールド(NOW)を取得します。
    • スタックの状態
      • [objectref(this)] =>
      • [objectref(this), value(Field NOW:Int)]

(8) 8: getstatic #4

  • getstatic命令(既出)を実行します
    • 操作内容 - クラスのstaticフィールド(BIRTH)を取得します。
    • スタックの状態
      • [objectref(this), value(Field NOW:Int)] =>
      • [objectref(this), value(Field NOW:Int), value(Field BIRTH:Int)]

(9) 11: isub

  • isub命令を実行します。
    • 操作内容 - オペランドスタックから二つ値をポップして、先頭のものから後ろのものを減算する
    • スタックの状態
      • [objectref(this), value(Field NOW:Int), value(Field BIRTH:Int)] =>
      • [objectref(this), result(Int)]

(10) 12: putfield #1

  • putfield命令を実行します
    • 操作内容 - オペランドスタックから二つ値をポップして、先頭の参照のフィールド#1( = Anniversary.age)に値を格納します
    • スタックの状態
      • [objectref(this), result(Int)] =>
      • []

(11) 15: return

  • return命令を実行します
    • 操作内容 - メソッドからvoidをリターンする
    • スタックの状態
      • [] =>
      • []

これによって、staticイニシャライザーに戻ります。

(12) 7: putstatic #10

  • putstatic命令を実行します
    • 操作内容 - オペランドスタックから値を取り出して、クラスのstaticフィールド#10 ( = Anniversary.ANN)に値を設定します
    • スタックの状態
      • [objectref(Anniversary)] =>
      • []

(13) 10: sipush 1995

  • sipush命令を実行します
    • 操作内容 - shortの値(1995)をオペランドスタックにプッシュします
    • スタックの状態
      • [] =>
      • [1995]

(14) 13: invokestatic #18

  • invokestatic命令を実行します
    • 操作内容 - テーブル#18( = java/time/Year.of:(I))のstaticメソッドを起動します
    • スタックの状態
      • [1995] =>
      • [objectref(java/time/Year)]

【追記 2015/04/26 17:08】

呼び出された方のjava/time/Year.of:(I)では最後にareturn命令が実行されるため、起動側スタックにobjectrefがプッシュされる。

  • aretrun
    • 操作内容 - メソッドからreferenceをリターンする

(15) 16: invokevirtual #19

  • invokevirtual命令を実行します
    • 操作内容 - テーブル#19 ( = java/time/Year.getValue:()I)を起動します
    • スタックの状態
      • [objectref(java/time/Year)] =>
      • [value(int)]

僕の持っているJava仮想マシン仕様第二版だと、オペランドスタックからobjectrefを取ってきて実行するように書かれているけど、今のスタックの状態だとオペランドスタック空っぽですよね…直前の結果を持っているスタック以外の1個だけ保持できる記憶領域あるのかしら、それとも(14)のinvokestaticオペランドスタックに一度積んでいるのかしら…(´・ω・`)<(仮想マシンまだよくわかってないマンです)

【追記 2015/04/26 17:08】

呼び出されたjava/time/Year.getValue:()Iで最後にireturn命令が実行されて、起動側スタックにintの値がプッシュされる。

(16) 19: putstatic #4

  • putstatic命令(既出)を実行します
    • 操作内容 - オペランドスタック(空だよね…)から値を取り出し、テーブル#4( = Anniversary.BIRTH:I)に値を格納します
    • スタックの状態
      • [value(int)] =>
      • []

(17) 22: invokestatic #20

  • invokestatic命令(既出)を実行します
    • 操作内容 - テーブル#20 ( = java/time/Year.now:())を起動します。
    • スタックの状態
      • [] =>
      • [objectref(java/time/Year)]

【追記 2015/04/26 17:08】

呼び出されたjava/time/Year.now:()が最後にareturnを実行して、起動側スタックにobjectrefがプッシュされる。

(18) 25: invokevirtual #19

  • invokevirtual命令(既出)を実行します
    • 操作内容 - テーブル#19 ( = java/time/Year.getValue:()I)を起動します(既出)
    • スタックの状態
      • [objectref(java/time/Year)] =>
      • [value(int)]

【追記 2015/04/26 17:08】

呼び出されたjava.time/Year.getValue:()Iが最後にireturnを実行して、起動側スタックにintの値がプッシュされる。

(19) 28: putstatic #3

  • putstatic命令(既出)を実行します
    • 操作内容 - オペランドスタック(空だよね…)から値を取り出し、テーブル#3( = Anniversary.NOW:I)に値を格納します
    • スタックの状態
      • [value(int)] =>
      • []

(20) 31: return

  • return命令(既出)を実行します。
    • 操作内容 - メソッドからvoidをリターンします
    • スタックの状態
      • [] =>
      • []

バイトコードを読んでみて

上記の操作(7)および(8)を見るとわかるように、staticフィールドの初期化で、とりあえずfinalなフィールドでも参照ならnull、プリミティブなら0で初期化されるという話と合わせて考えると、上記のクイズで答えが0になるのも納得できますね。


結論

バイトコード読むの面白いのでやってみるべき。

参考文献

amazon.co.jp

【修正 2015/04/27 12:39】

命令へのリンクがType Checking Instructionsに向かっていたのを全面的にMachine Instruction Setsに向かうように修正した。