知らなくても困らない Javaクラスのバイトコードの読み方


普段使いでは困ることはないですが、昨今はバイトコードマニピュレーションによる黒魔術が謳歌しているため、知っていると役に立つ場合もあるバイトコードの最低限の読み方を説明します。

クラスファイルの中身

以下のような簡単なソースコードを考えましょう。

public class Class1 {
    public int add(int x, int y) {
        return x + y;
    }
}


このソースコードコンパイルして作成された Class1.class の中身のダンプを見てみます。

$ hexdump -C Class1.class

00000000  ca fe ba be 00 00 00 34  00 15 0a 00 03 00 12 07  |.......4........|
00000010  00 13 07 00 14 01 00 06  3c 69 6e 69 74 3e 01 00  |........<init>..|
00000020  03 28 29 56 01 00 04 43  6f 64 65 01 00 0f 4c 69  |.()V...Code...Li|
00000030  6e 65 4e 75 6d 62 65 72  54 61 62 6c 65 01 00 12  |neNumberTable...|
00000040  4c 6f 63 61 6c 56 61 72  69 61 62 6c 65 54 61 62  |LocalVariableTab|
00000050  6c 65 01 00 04 74 68 69  73 01 00 08 4c 43 6c 61  |le...this...LCla|
00000060  73 73 31 3b 01 00 03 61  64 64 01 00 05 28 49 49  |ss1;...add...(II|
00000070  29 49 01 00 01 78 01 00  01 49 01 00 01 79 01 00  |)I...x...I...y..|
00000080  0a 53 6f 75 72 63 65 46  69 6c 65 01 00 0b 43 6c  |.SourceFile...Cl|
00000090  61 73 73 31 2e 6a 61 76  61 0c 00 04 00 05 01 00  |ass1.java.......|
000000a0  06 43 6c 61 73 73 31 01  00 10 6a 61 76 61 2f 6c  |.Class1...java/l|
000000b0  61 6e 67 2f 4f 62 6a 65  63 74 00 21 00 02 00 03  |ang/Object.!....|
000000c0  00 00 00 00 00 02 00 01  00 04 00 05 00 01 00 06  |................|
000000d0  00 00 00 2f 00 01 00 01  00 00 00 05 2a b7 00 01  |.../........*...|
000000e0  b1 00 00 00 02 00 07 00  00 00 06 00 01 00 00 00  |................|
000000f0  01 00 08 00 00 00 0c 00  01 00 00 00 05 00 09 00  |................|
00000100  0a 00 00 00 01 00 0b 00  0c 00 01 00 06 00 00 00  |................|
00000110  42 00 02 00 03 00 00 00  04 1b 1c 60 ac 00 00 00  |B..........`....|
00000120  02 00 07 00 00 00 06 00  01 00 00 00 03 00 08 00  |................|
00000130  00 00 20 00 03 00 00 00  04 00 09 00 0a 00 00 00  |.. .............|
00000140  00 00 04 00 0d 00 0e 00  01 00 00 00 04 00 0f 00  |................|
00000150  0e 00 02 00 01 00 10 00  00 00 02 00 11           |.............|
0000015d


なるほど、良くわかりませんね。


区分毎に色分けしてみましょう。

f:id:Naotsugu:20171102231005p:plain

それぞれ大雑把には以下の意味合いとなります。

  • マジックナンバー(紺色)
    • ca fe ba be 固定でクラスファイルの識別として利用される
  • マイナーバージョン・メジャーバージョン(青色)
    • 34 は10進だと52で、これは Java8 となる
  • コンスタントプールカウント(水色)
    • 以降に続くコンスタントプールのエントリ数 + 1 の値となる
    • 15 は10進で21なので、コンスタントプールは20エントリ存在するという意味
  • コンスタントプール(赤)
    • クラスファイル内から参照される様々な文字列定数が定義される
  • アクセスフラグ, クラス・スーパークラスの識別(コンスタントプールの参照値),インターフェースカウントインターフェーステーブル(桃色)
    • クラスの各種属性が定義される
    • インターフェーステーブルは今回の例では無し
  • フィールドカウント(橙色)
    • 以降のフィールドテーブルのエントリ数
    • 今回の例ではフィールドを定義していないので無し
  • メソッドカウント(緑色)
    • 以降のメソッドテーブルのエントリ数
    • 今回の例ではデフォルトコンストラクタと add メソッドの2つ
  • メソッドテーブル(黄緑色)
    • 黄色の箇所がバイトコードの命令群となる(Code attribute の code配列の中身)
  • クラスアトリビュート(藤色)
    • クラスの属性情報(可変エントリ)

図中の黄色の箇所がバイトコードの命令となっています。2a b7 00 01 b1 は具体的には以下のニーモックに対応します。

  • 42(0x2a) aload_0
  • 183(0xb7) invokespecial
    • 続くコンスタントプールのインデックス01 のメソッドを実行
  • 177(0xb1) return

これらの詳細については後ほど見ていきます。

クラスファイルの構造

先に見たバイナリは、JDK の中では ClassFile構造体として表現され、以下のように定義されています。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

cp_info がコンスタントプールです。

field_info の中にクラスのフィールドの情報が入っており、method_info の中にメソッドに関する情報が入っています(前述の黄緑色の部分ですね)。

attribute_info にはこのクラスの属性値(内部クラスの情報など)が色々定義されます。

field_info の中やmethod_info の中にもさらに入れ子になった属性(attributee)が色々と定義されます。


クラスファイルの構造体の中身を図示すると以下のようになります。

f:id:Naotsugu:20171102231032p:plain

ここで、水色のボックスが属性(Attribute)を表しています。属性は任意の属性を追加定義できるようになっており、例えば Java5 で追加されたアノテーション用には RuntimeVisibleAnnotations といった属性が追加されたりといった具合になります。

属性は必要なもののみが定義され、全てが全てクラスファイル中に存在するわけではありません。

ちなみにCode属性の中のcode配列(黄色のボックス)の中に先程見た2a b7 00 01 b1 といった命令が入っています。


これらの属性は属性毎に構造体として定義されており、バイナリ長もそれぞれになるので、クラスファイルのバイナリダンプを読むのはとても骨が折れる作業になります。

javap コマンド

バイナリダンプを読むのにうんざりしたら javap コマンドを使います。

Javap コマンドはクラスファイルを逆アセンブルして読みやすい形で表示することができます。


よく使うオプションは以下になります。

オプション 説明
-help -? 使用方法のメッセージを出力する
-c クラスのメソッド毎に逆アセンブルしたニーモックを表示する
-p -private すべてのクラスとメンバーを表示する(指定しないとpublicのみ表示)
-v -verbose -c に加え、コンスタントプールや メソッドのスタックサイズ、localsargs の数などの詳細表示する


まぁ、たいてい -v しておけば事足ります。

$ javap -v Class1.class


さきほどのクラスファイルを Javap すると以下のような出力が得られます。

Classfile java/main/Class1.class
  Last modified 20XX/XX/XX; size 349 bytes
  MD5 checksum 0230ca9e6ca3777ff46cab466836bb65
  Compiled from "Class1.java"
public class Class1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#18 // java/lang/Object."<init>":()V
   #2 = Class              #19    // Class1
   #3 = Class              #20    // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               LClass1;
  #11 = Utf8               add
  #12 = Utf8               (II)I
  #13 = Utf8               x
  #14 = Utf8               I
  #15 = Utf8               y
  #16 = Utf8               SourceFile
  #17 = Utf8               Class1.java
  #18 = NameAndType        #4:#5  // "<init>":()V
  #19 = Utf8               Class1
  #20 = Utf8               java/lang/Object
{
  public Class1();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LClass1;

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   LClass1;
            0       4     1     x   I
            0       4     2     y   I
}
SourceFile: "Class1.java"


javap 出力の概要

Javap の出力内容を順番に見ていきましょう。


最初は対象クラスファイルのシステム情報です。

lassfile java/main/Class1.class
  Last modified 20XX/XX/XX; size 349 bytes
  MD5 checksum 0230ca9e6ca3777ff46cab466836bb65
  Compiled from "Class1.java"

これはクラスファイルの中身というよりは、クラスファイル自体のシステム情報です。


続いてクラスファイルのバージョンやアクセスフラグなどの情報が続きます。

public class Class1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

メジャーバージョンの定義は以下のようになります。

ACC_PUBLIC (0x0001)はこのクラスがpublic宣言されていることを意味し、ACC_SUPER (0x0020)は古いコンパイラにより生成されたクラスファイルには設定されないが、現在は全てこのフラグが設定される(下位互換用でinvokespecial 命令により起動された場合の振る舞いに影響)。

フラグはこの他に、ACC_FINAL(0x0010)、 ACC_INTERFACE(0x0200)、ACC_ABSTRACT(0x0400) があります。


続いてコンスタントプールの情報です。

Constant pool:
   #1 = Methodref          #3.#18 // java/lang/Object."<init>":()V
   #2 = Class              #19    // Class1
   #3 = Class              #20    // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               LClass1;
  #11 = Utf8               add
  #12 = Utf8               (II)I
  #13 = Utf8               x
  #14 = Utf8               I
  #15 = Utf8               y
  #16 = Utf8               SourceFile
  #17 = Utf8               Class1.java
  #18 = NameAndType        #4:#5  // "<init>":()V
  #19 = Utf8               Class1
  #20 = Utf8               java/lang/Object

クラスファイル中では、ここで定義された文字列定数を #1 #2 といったインデックスで参照して様々な箇所から参照しています。

例えば return "Hello"; といったコードがあれば、Hello という値がコンスタントプール内に定義されますし、外部クラスのメソッド呼び出しなどのクラス名やメソッド名などもここに定数として定義されます。


続いて method_info の中身になります(今回はフィールドが無いのでfield_info はありません)。

最初はコンパイラが追加したデフォルトコンストラクタの内容になります。

  public Class1();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LClass1;

詳細は後ほど説明するので次にいきます。


続いてソースコードに定義した add メソッドの内容になります。

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   LClass1;
            0       4     1     x   I
            0       4     2     y   I

こちらも後ほど。


最後がクラス自身の属性である attribute_info になります。

SourceFile: "Class1.java"

今回は SourceFile 属性の内容だけですね。

先程見たクラスファイルのバイナリ出力の結果の最後の部分をもう一度見てみると、00 10 00 00 00 02 00 11 となっています。最初の 0x10 は10進で16なのでコンスタントプールの #16 = Utf8 SourceFile を参照しています。

最後の 0x11 は10進で17なので、コンスタントプールの #17 = Utf8 Class1.java を参照しており、結果、SourceFile 属性の値が Class1.java として出力されているという具合です。



ではバイトコード命令を読んでいきますが、その前に前提知識として 2つ程知っておかなければならないことがあります。

型とメソッドの読み方

最初に型の表記方法を覚えておく必要があります。


コンスタントプールでは型の表記を略式の記号で表現します。

以下のルールとなっています。

Type descriptor 型 / 説明
Z boolean
C char
B byte
S short
I int
F float
J long
D double
L; reference / Object の場合 Ljava/lang/Object; となる
[ reference / 一次元配列で int配列の場合 [I となる
[[ reference / 二次元配列で Object配列の場合 [[java/lang/Object; となる


上記型記号を使い、メソッドは以下のように表現されます。

メソッドシグネチャ Method descriptor
void m(int i, float f) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I)Ljava/lang/Object;


例えば、今回出力した javap のコンスタントプールにある#12 = Utf8 (II)Iint add(int x, int y) というメソッドシグネチャの型情報を表現するものになります。


さらに例を上げると #5 = Utf8 ()Vvoid m() というメソッドシグネチャの型情報を表現するものになります。[[I はint型の2次元配列です。読みにくいですが我慢しましょう。

オペランドスタック

もう一つ事前に知っておくべきものとして、Java仮想マシンの命令はオペランドスタックを介して実行されるということです。

Javaでは、スレッド毎に以下のようなメモリ領域が(Javaヒープとは別に)確保されます。

f:id:Naotsugu:20171102231116p:plain

メソッドの呼び出しが行われると、JVMスタックの中にFrameがPushされます。メソッドの終了時には現在のFrameがPopされて破棄されます。

Frameが積み上がってJVMスタックが一杯になると、おなじみの StackOverflow になります。

図中右側に示した Frame の中にはオペランドスタックがあり、仮想マシンの命令によりPushしたりPopしたりしながら処理が行われていきます。

アセンブラなどでは用途別に用意されたレジスタを介して命令が実行されますが、Javaはシングルスタックマシンなので、全て1つのスタックで処理をまかなっています。


整数の加算を例に図示すると以下のように動作します。

f:id:Naotsugu:20171102231131p:plain

このようにオペランドスタックへのPushとPopを繰り返しながら命令が実行されていきます。

コンストラクタの実行

さて、前置きが長くなりました。


今回の例のコンストラクタ部分を見ていきましょう。

  public Class1();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LClass1;

頭の箇所はメソッド定義に関する記載です。

  • descriptor はメソッドシグネチャ()V つまり、void m() となる
  • flags は ACC_PUBLIC で、このメソッドは public 定義

Code の中身が CodeAttribute 構造体の中身となります。

LineNumberTable はコード行との対応で、LocalVariableTable はメソッド内で宣言した変数の名前などが記録されています(主にデバッグ情報として利用されます)。


中ほどの以下は何でしょう。

      stack=1, locals=1, args_size=1

それぞれ以下の意味です。があまり気にしなくて大丈夫です。

  • stack このメソッドの実行で必要なスタックの深さ
  • locals ローカル変数テーブルに予約する必要があるローカル変数スロットの数
  • arg_size この処理で参照するパラメータの数

なお、コンストラクタはコード上は引数無しですが、クラスファイル上では自身のオブジェクトを第一引数に取ります。これは他のメソッドでも同様です。


さてニーモック部分です。

 0: aload_0
 1: invokespecial #1  // Method java/lang/Object."<init>":()V
 4: return
  • aload_0 ローカル変数配列[0] (自身のオブジェクト)をオペランドスタックにプッシュ
    • aload_0 の先頭文字 a は参照を表す
    • iload であれば int 、dload であれば double といった具合
  • invokespecial により親クラスのコンストラクタを呼び出す
    • #1 はコンスタントプールの当該スロットの参照(java/lang/Object."<init>":()V )
  • return で戻り値なしでメソッドを抜ける

先頭の数字は当該命令が先頭から何バイト目に当たるかを示しています。

加算メソッド

次に加算メソッドの命令を見てみましょう。

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   LClass1;
            0       4     1     x   I
            0       4     2     y   I

形はコンストラクタの場合と同じですね。


ニーモック部分を見ます。

 0: iload_1
 1: iload_2
 2: iadd
 3: ireturn

こちらは先程の図で説明した通りです。

  • iload ローカル変数配列から int 値をロード
    • _1 はローカル変数配列のインデックスを表す
    • インデックスで示されたローカル変数の値をオペランドスタックへプッシュ
    • LocalVariableTable の項目にあるように、インデックス0 の引数は自身のクラスオブジェクト
    • インデックス1 が引数の x
  • iload ローカル変数配列から int 値をロード
    • インデックスの2で示されたローカル変数の値をオペランドスタックへプッシュ
  • iadd オペランドスタックから値を2つPopして加算結果をオペランドスタックへプッシュ
  • ireturn メソッドから int をリターン
    • カレントフレームのオペランドスタックから値をPopし、起動側フレームのスタックへPush
    • カレントメソッドのオペランドスタックにある値は全て破棄される

以上が処理の内容になります。細かく見ればとても簡単ですね。

invoke 系命令

この後、もう少し色々な例を見ていきます。

が、その前に invoke 系命令につて示しておきます。

なぜこんなに色々あるかと言うと、どのインスタンスのメソッドを呼び出すかのメソッドルックアップのやり方が異なるためです。

条件判断

もう少し例を見ていきましょう。

以下のコードを考えます。

    public boolean isNull(Object object) {
        return object == null;
    }


javap のメソッド部分は以下のようになります。

  public boolean isNull(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Z
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: ifnonnull     8
         4: iconst_1
         5: goto          9
         8: iconst_0
         9: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LClass1;
            0      10     1 object   Ljava/lang/Object;
      StackMapTable: number_of_entries = 2
        frame_type = 8 /* same */
        frame_type = 64 /* same_locals_1_stack_item */
          stack = [ int ]


ニーモック部分を抜き出しました。

 0: aload_1
 1: ifnonnull     8
 4: iconst_1
 5: goto          9
 8: iconst_0
 9: ireturn

順に見ていきましょう。

  • aload_1 ローカル変数配列[1]をオペランドスタックにプッシュ
    • 対象は isNull(Object object) の引数のオブジェクトの参照(実オブジェクトはヒープ中に存在)
  • ifnonnull オペランドスタックからポップした値が null でなければ 8へ分岐
    • そうでなければ次の命令に進む
  • iconst_0 int 定数 0オペランドへプッシュ
  • ireturn int 値をオペランドスタックからポップし、呼び出し元フレームのスタックにプッシュ

8への分岐がなかった場合は

  • iconst_1 int 定数 1オペランドへプッシュ
  • goto 無条件分岐で 9 へ分岐
  • ireturn int 値をオペランドスタックからポップし、呼び出し元フレームのスタックにプッシュ

といった具合です。


もう一つ、今までと異なり StackMapTable 属性が出力されています。

この属性は Java6 で追加された属性で、ある時点でのローカル変数とスタックの値の型の情報が入っています。

「ある時点」とは分岐によるジャンプの発生時で、ここでの例だと、ifnonnullgoto の2つですね。

goto の時点で、stack = [ int ] となっており、スタックには int 型変数が入っているはずだということになります。クラスファイルの検証用途で利用されます。

for ループ

最後にもう一つ、ループ処理を見てみましょう。

以下のコードを考えます。

    public int sum(int num) {
        int sum = 0;
        for(int i = 1; i <= num; i++) {
            sum += i;
        }
        return sum;
    }


javap のメソッド部分は以下のようになります。

  public int sum(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: iconst_0
         1: istore_2
         2: iconst_1
         3: istore_3
         4: iload_3
         5: iload_1
         6: if_icmpgt     19
         9: iload_2
        10: iload_3
        11: iadd
        12: istore_2
        13: iinc          3, 1
        16: goto          4
        19: iload_2
        20: ireturn
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 9
        line 4: 13
        line 7: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4      15     3     i   I
            0      21     0  this   LClass1;
            0      21     1   num   I
            2      19     2   sum   I
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 4
          locals = [ int, int ]
        frame_type = 250 /* chop */
          offset_delta = 14


最初に以下のコードの箇所を見ていきます。

int sum = 0;

ニーモック部分の該当箇所は以下です。

 0: iconst_0
 1: istore_2
  • iconst_0 int 定数 0オペランドスタックへプッシュ
  • istore_2 オペランドスタックからポップしてローカル変数配列[2]にストア
    • ローカル変数配列[0] は this、ローカル変数配列[1] はメソッド引数が入っている


次に以下の部分です。

for(int i = 1; i <= num; i++)


i++ の前までの部分のニーモック部分です。

 2: iconst_1
 3: istore_3
 4: iload_3
 5: iload_1
 6: if_icmpgt     19
  • iconst_1 int 定数 1オペランドスタックへプッシュ
  • istore_3 オペランドスタックからポップしてローカル変数配列[3]にストア
  • iload_3 ローカル変数配列[3]をオペランドスタックにプッシュ
  • iload_1 ローカル変数配列[1]をオペランドスタックにプッシュ
    • これはメソッドの引数num の値
    • この時点でオペランドスタックは 「i」「num」の値が積まれている
  • if_icmpgt オペランドスタックから値を2つポップし、value1 > value2 の場合 19 へ分岐


合計の箇所に移ります。

    sum += i;


ニーモックの該当箇所は以下となります。

 9: iload_2
10: iload_3
11: iadd
12: istore_2
  • iload_2 ローカル変数配列[2]をオペランドスタックにプッシュ
    • これはローカル変数 sum の値
  • iload_3 ローカル変数配列[3]をオペランドスタックにプッシュ
    • これはローカル変数 i の値
  • iadd オペランドスタックから値を2つPopして加算結果をオペランドスタックへプッシュ
  • istore_2 オペランドスタックからポップしてローカル変数配列[2]にストア
    • 現在までの合計値がローカル変数配列[2] に入る


次にfor文のインクリメント部分です。

for(int i = 1; i <= num; i++)


i++ のニーモック部分です。

13: iinc          3, 1
16: goto          4
  • iinc ローカル変数配列[3]をインクリメント
  • goto 無条件分岐で 4 へ分岐
    • 戻って for 文を繰り返す

最後に if_icmpgt19 に分岐したら、

 6: if_icmpgt     19
  ...
19: iload_2
20: ireturn
  • iload_2 ローカル変数配列[2]をオペランドスタックにプッシュ
    • これはローカル変数 sum の値
  • ireturn int 値をオペランドスタックからポップし、呼び出し元フレームのスタックにプッシュ

以上で合計値が呼び出し元に返ります。

まとめ

簡単な例でクラスファイルの中身と Java仮想マシンの命令実行過程を見てみました。

オペコードはここで見たものの他にもたくさんありますが、https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html にあるオペコードの仕様を見れば、同じように読むことができます。