LoginSignup
14
11

More than 5 years have passed since last update.

Java クラスファイルの構成 その4

Last updated at Posted at 2016-02-02

Code 属性

Code 属性はメソッドに付与される属性で、その名の通りプログラムのコードを表現しています。
Code 属性の本体部分 (data の部分) は以下の様な構造となっています。

code_attribute_data {
  max_stack           // 2 byte
  max_locals          // 2 byte
  bytecode
  exceptions_table
  attributes
}

max_stack はこのコードを実行する際に使われる、オペランドスタックの深さの最大値です。
max_locals はこのコードを実行する際に必要となる、ローカル変数を格納する領域の大きさの最大値です。
どちらについても long 及び double は2つ分の領域を利用する点に注意する必要があります。

Java 仮想機械は スタックマシン と呼ばれるアーキテクチャを採用しています。
これは、レジスタの代わりにスタック (オペランドスタックと呼びます) を持つようなアーキテクチャです。
例えば、スタックマシンにおける加算命令は、スタックから2つポップして加算を実行し、結果をスタックにプッシュするといった命令となります。
加算対象や結果を格納するレジスタを指定する必要が無いため、命令長が短くなるという利点があります。

Java 仮想機械はメソッドを呼んだ際に フレーム と呼ばれるメモリ領域をスタック領域から確保します。
各フレームはローカル変数の配列とオペランドスタック(と実行時定数プールへの参照)を持ちます。
つまり、max_stack と max_locals は必要なフレームのサイズを示していると言えます。

bytecode はこの属性の本体部分で、以下の様な構造となっています。

bytecode {
  length        // 4 byte
  instructions  // length byte
}

length は命令列の長さを表しており、0 より大きくなければなりません。
instructions はプログラムを構成する命令列です。
命令列を構成する命令についての説明は後に回します。

exceptions_table は try-catch や try-finally を表現したもので、先頭 2 バイトに個数が入っており、その後に指定された個数だけ情報が格納されているという構造をしています。
exceptions_table 及びその各エントリの構造は以下の様になっています。

exceptions_table {
  table_length          // 2 byte
  exceptions_entries    // table_length 個の exceptions_entry
}
exceptions_entry {
  start_pc              // 2 byte
  end_pc                // 2 byte
  handler_pc            // 2 byte
  catch_type            // 2 byte
}

start_pc, end_pc は instructions のインデックスで、try 節の範囲を示します。
try 節の範囲は start_pc から end_pc の直前までとなっています。
handler_pc は instructions のインデックスで、例外ハンドラのアドレスを示します。
catch_type は 0 または定数プール上のインデックスで、キャッチする例外の型を表現します。
catch_type が 0 の場合は、そのエントリは try-finally 節であることを表しています。

命令

Java バイトコードの各命令は(というより機械語命令は)、1つのオペコードと0個以上のオペランドからなります。
オペコードは命令の種類を指定するもので、オペコードにわかり易い名前をつけたものをニーモニックと呼びます。
以下では、それぞれのニーモニックについて簡単に解説します。

何もしない命令

nop (0x00)

何もしない命令です。
nop がニーモニックで、対応するオペコードが 0x00 (16進数で0) です。
オペランドは持ちません。

定数ロード命令

aconst_null (0x01)

スタックに null をロードします。

iconst_ 系列 (0x02-0x08)

iconst_m1 (0x02), iconst_0 (0x03), iconst_1 (0x04), iconst_2 (0x05), iconst_3 (0x06), iconst_4 (0x07), iconst_5 (0x08) の7つです。
それぞれ int定数の -1, 0, 1, 2, 3, 4, 5 をスタックにロードします。

lconst_ 系列, fconst_ 系列, dconst_ 系列 (0x09-0x0f)

lconst_0 (0x09), lconst_1 (0x0a) はそれぞれ long 定数の 0, 1 をスタックにロードします。
fconst_0 (0x0b), fconst_1 (0x0c), fconst_2 (0x0d) はそれぞれ float 定数の 0, 1, 2 をスタックにロードします。
dconst_0 (0x0e), dconst_1 (0x0f) はそれぞれ double 定数の 0, 1 をスタックにロードします。

bipush (0x10)

1バイトのオペランドを取ります。
オペランドの値をスタックにロードします。

sipush (0x11)

2バイトのオペランドを取ります。
オペランドの値をスタックにロードします。

ldc (0x12)

1バイトのオペランドを取ります。
(実行時)定数プールから定数値をスタックにロードします。
オペランドの値はロードする定数の定数プール上のインデックスです。
この命令でロードできる定数は、その型が int, float, String, Class, MethodType, MethodHandle であるもののみです。
long 及び double 型の定数のロードには ldc2_w を利用します。

ldc_w (0x13)

2バイトのオペランドを取ります。
この命令は ldc 命令と同じ働きをしますが、より大きい値をオペランドに持つことができます。
バイトコードのサイズを小さくするために、通常は ldc 命令を優先的に利用します。

ldc2_w (0x14)

2バイトのオペランドを取ります。
定数プールから long または double 定数値をスタックにロードします。

ローカル変数ロード命令

iload (0x15), lload (0x16), fload (0x17), dload (0x18), aload (0x19)

1バイトのオペランドを取ります。
現在のフレームのローカル変数配列上のオペランドで指定されたインデックスの値をスタックにロードします。
iload は int 型、lload は long 型、fload は float 型、dload は double 型、aload は参照型 (オブジェクトの型) の値をロードします。
long 及び double は他の倍のサイズであるため、指定されたインデックスの次のインデックスまでロードすることに注意して下さい。
wide 命令と組み合わせて用いることで、オペランドを 2 バイトに拡張できます。

iload_ 系列, lload_ 系列, fload_ 系列, dload_ 系列, aload_ 系列 (0x1a-0x2d)

これらの命令は iload, lload, fload, dload, aload に 0, 1, 2, 3 の引数を渡したものと同じ働きをします。

ニーモニックとオペコードの対応は以下のとおりです。
iload_0 (0x1a), iload_1 (0x1b), iload_2 (0x1c), iload_3 (0x1d),
lload_0 (0x1e), lload_1 (0x1f), lload_2 (0x20), lload_3 (0x21),
fload_0 (0x22), fload_1 (0x23), fload_2 (0x24), fload_3 (0x25),
dload_0 (0x26), dload_1 (0x27), dload_2 (0x28), dload_3 (0x29),
aload_0 (0x2a), aload_1 (0x2b), aload_2 (0x2c), aload_3 (0x2d)

long 及び double は他の倍のサイズであることに注意して下さい。
例えば、dload_1 は ローカル変数配列の 1 番目と 2 番目にかけて格納されている double 値をスタックにロードします。

配列要素取得命令

iaload (0x2e), laload (0x2f), faload (0x30), daload (0x31), aaload (0x32), baload (0x33), caload (0x34), saload (0x35)

これらの命令は、配列から要素を取得する命令です。
スタックから2つの値をポップし、1つ目(スタックの上にあったほう)を配列のインデックス n 、2つ目を配列への参照 array として、 array[n] の値をスタックにプッシュします。
それぞれの命令は配列の要素の型によって使い分けます。
iaload は int配列、laload は long 配列、faload は float 配列、daload は double 配列、aaload は参照型の配列、baload は byte 配列、caload は char 配列、saload は short 配列に対して用います。

ローカル変数ストア命令

istore (0x36), lstore (0x37), fstore (0x38), dstore (0x39), astore (0x3a)

1バイトのオペランドを取ります。
スタックから値をポップしてオペランドで指定されたインデックスのローカル変数に格納します。
istore は int 型、lstore は long 型、fstore は float 型、dstore は double 型、astore は参照型 (オブジェクトの型) の値を格納します。
long 及び double は他の倍のサイズであるため、指定されたインデックスの次のインデックスまで利用する点に注意する必要があります。
wide 命令と組み合わせて用いることで、オペランドを 2 バイトに拡張できます。

istore_ 系列, lstore_ 系列, fstore_ 系列, dstore_ 系列, astore_ 系列 (0x3b-0x4e)

これらの命令は istore, lstore, fstore, dstore, astore に 0, 1, 2, 3 の引数を渡したものと同じ働きをします。

ニーモニックとオペコードの対応は以下のとおりです。
istore_0 (0x3b), istore_1 (0x3c), istore_2 (0x3d), istore_3 (0x3e),
lstore_0 (0x3f), lstore_1 (0x40), lstore_2 (0x41), lstore_3 (0x42),
fstore_0 (0x43), fstore_1 (0x44), fstore_2 (0x45), fstore_3 (0x46),
dstore_0 (0x47), dstore_1 (0x48), dstore_2 (0x49), dstore_3 (0x4a),
astore_0 (0x4b), astore_1 (0x4c), astore_2 (0x4d), astore_3 (0x4e)

配列要素格納命令

iastore (0x4f), lastore (0x50), fastore (0x51), dastore (0x52), aastore (0x53), bastore (0x54), castore (0x55), sastore (0x56)

これらの命令は、配列に値を格納する命令です。
スタックから3つの値 (スタックの上から順にvalue, n, array とする) をポップし、array[n] に value を格納します。
それぞれの命令は配列の要素の型によって使い分けます。
iastore は int配列、lastore は long 配列、fastore は float 配列、dastore は double 配列、aastore は参照型の配列、bastore は byte 配列、castore は char 配列、sastore は short 配列に対して用います。

スタック操作命令

pop (0x57), pop2 (0x58)

スタックから値をポップします。pop は1つ、pop2 は2つポップします。
long や double は値2つ分とみなされることに注意して下さい。
以降のスタック操作命令についても同様です。

dup (0x59), dup_x1 (0x5a), dup_x2 (0x5b)

スタックのトップの要素を複製し、スタックの適切な位置に挿入します。
dup の場合、上から順に A, ... となっているスタックを A, A, ... にします。
dup_x1 の場合、上から順に A, B, ... となっているスタックを A, B, A, ... にします。
dup_x2 の場合、上から順に A, B, C, ... となっているスタックを A, B, C, A, ... にします。

dup2 (0x5c), dup2_x1 (0x5d), dup2_x2 (0x5e)

スタックのトップの2要素を複製し、スタックの適切な位置に挿入します。
dup2 の場合、上から順に A, B, ... となっているスタックを A, B, A, B, ... にします。
dup2_x1 の場合、上から順に A, B, C, ... となっているスタックを A, B, C, A, B, ... にします。
dup2_x2 の場合、上から順に A, B, C, D, ... となっているスタックを A, B, C, D, A, B, ... にします。

swap (0x5f)

スタックのトップの2要素をスワップします。
要するに、上から順に A, B, ... となっているスタックを B, A, ... にします。

算術演算命令

iadd (0x60), ladd (0x61), fadd (0x62), dadd (0x63)

スタックから2つポップして加算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 + v1), ... にします。
iadd は int 型、ladd は long 型、fadd は float 型、dadd は double 型の加算命令です。
以降の算術演算命令についても、同様に頭文字で型を判別します。

isub (0x64), lsub (0x65), fsub (0x66), dsub (0x67)

スタックから2つポップして減算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 - v1), ... にします。

imul (0x68), lmul (0x69), fmul (0x6a), dmul (0x6b)

スタックから2つポップして乗算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 * v1), ... にします。

idiv (0x6c), ldiv (0x6d), fdiv (0x6e), ddiv (0x6f)

スタックから2つポップして除算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 / v1), ... にします。

irem (0x70), lrem (0x71), frem (0x72), drem (0x73)

スタックから2つポップして剰余算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 % v1), ... にします。

ineg (0x74), lneg (0x75), fneg (0x76), dneg (0x77)

スタックから1つポップし、その値の正負を反転させた値をスタックにプッシュする命令です。
上から v1, ... となっているスタックを (- v1), ... にします。

論理演算命令

ishl (0x78), lshl (0x79)

スタックから2つポップして左シフト演算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 << v1), ... にします。
ishl は int 型、lshl は long 型の左シフト命令です。
以降の論理演算命令についても、同様に頭文字で型を判別します。

ishr (0x7a), lshr (0x7b)

スタックから2つポップして右算術シフト演算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 >> v1), ... にします。

iushr (0x7c), lushr (0x7d)

スタックから2つポップして右論理シフト演算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 >>> v1), ... にします。

iand (0x7e), land (0x7f)

スタックから2つポップして論理積演算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 & v1), ... にします。

ior (0x80), lor (0x81)

スタックから2つポップして論理和演算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 | v1), ... にします。

ixor (0x82), lxor (0x83)

スタックから2つポップして排他的論理和演算を実行し、結果をスタックにプッシュする命令です。
上から v1, v2, ... となっているスタックを (v2 ^ v1), ... にします。

ローカル変数の加算命令

iinc (0x84)

1バイトのオペランドを2つ取ります。
1つ目のオペランドは現在のフレームのローカル変数配列のインデックスで、2つ目のオペランドは加算を行う符号付きの byte 値です。
この命令は指定されたインデックスのローカル変数に、指定された値を加算します。
wide 命令と組み合わせて用いることで、両オペランドを 2 バイトに拡張できます。

プリミティブ型の型変換命令

i2l (0x85), i2f (0x86), i2d (0x87), l2i (0x88), l2f (0x89), l2d (0x8a), f2i (0x8b), f2l (0x8c), f2d (0x8d), d2i (0x8e), d2l (0x8f), d2f (0x90), i2b (0x91), i2c (0x92), i2s (0x93)

プリミティブ型同士の型変換命令です。
スタックから値をポップして、型変換を実行し、結果をスタックにプッシュします。
ニーモニックの最初の文字は変換前の型を表し、最後の文字は変換後の型を表します。
文字と型の対応は、
i : int, l : long, f : float, d : double, b : byte, c : char, s : short
です。

比較演算命令

lcmp (0x94)

スタックから long 値を2つポップし、それらの値を比較します。
2つの long 値をポップした順に v1, v2 とすると、
v1 > v2 のとき、結果として -1 をプッシュします。
v1 = v2 のとき、結果として 0 をプッシュします。
v1 < v2 のとき、結果として 1 をプッシュします。

fcmpl (0x95), fcmpg (0x96)

スタックから float 値を2つポップし、それらの値を比較します。
2つの float 値をポップした順に v1, v2 とすると、
v1 > v2 のとき、結果として -1 をプッシュします。
v1 = v2 のとき、結果として 0 をプッシュします。
v1 < v2 のとき、結果として 1 をプッシュします。
v1, v2 の少なくとも一方が NaN のとき、fcmpl は -1 を、fcmpg は 1 を結果としてスタックにプッシュします。
fcmpl と fcmpg の違いは NaN を比較するときの振る舞いのみです。

dcmpl (0x97), dcmpg (0x98)

スタックから double 値を2つポップし、それらの値を比較します。
2つの double 値をポップした順に v1, v2 とすると、
v1 > v2 のとき、結果として -1 をプッシュします。
v1 = v2 のとき、結果として 0 をプッシュします。
v1 < v2 のとき、結果として 1 をプッシュします。
v1, v2 の少なくとも一方が NaN のとき、dcmpl は -1 を、dcmpg は 1 を結果としてスタックにプッシュします。

条件分岐命令

if 系列 (0x99-0x9e)

2バイトの符号付きのオペランドを取ります。
スタックから int 値をポップし(value とする)、それと 0 を比較して条件を満たしていれば分岐をします。
分岐先はこの命令のアドレスに、オペランドで指定されたオフセットを加えた位置です。
各ニーモニックと分岐条件の対応は以下のとおりです。

ifeq (0x99) : value == 0 なら分岐,
ifne (0x9a) : value != 0 なら分岐,
iflt (0x9b) : value < 0 なら分岐,
ifge (0x9c) : value >= 0 なら分岐,
ifgt (0x9d) : value > 0 なら分岐,
ifle (0x9e) : value <= 0 なら分岐

if_icmp 系列 (0x9f-0xa4)

2バイトの符号付きのオペランドを取ります。
スタックから2つの int 値をポップし(上から v1, v2 とする)、それらを比較して条件を満たしていれば分岐をします。
分岐先はこの命令のアドレスに、オペランドで指定されたオフセットを加えた位置です。
各ニーモニックと分岐条件の対応は以下のとおりです。

if_icmpeq (0x9f) : v2 == v1 なら分岐,
if_icmpne (0xa0) : v2 != v1 なら分岐,
if_icmplt (0xa1) : v2 < v1 なら分岐,
if_icmpge (0xa2) : v2 >= v1 なら分岐,
if_icmpgt (0xa3) : v2 > v1 なら分岐,
if_icmple (0xa4) : v2 <= v1 なら分岐

if_acmp 系列 (0xa5-0xa6)

2バイトの符号付きのオペランドを取ります。
スタックから2つのオブジェクトをポップし(上から v1, v2 とする)、それらを比較して条件を満たしていれば分岐をします。
分岐先はこの命令のアドレスに、オペランドで指定されたオフセットを加えた位置です。
各ニーモニックと分岐条件の対応は以下のとおりです。

if_acmpeq (0xa5) : v2 == v1 なら分岐,
if_acmpne (0xa6) : v2 != v1 なら分岐

ジャンプ命令

goto (0xa7)

2バイトの符号付きのオペランドを取ります。
この命令のアドレスに、オペランドで指定されたオフセットを加えた位置にジャンプします。

jsr (0xa8)

2バイトの符号付きのオペランドを取ります。
この命令のアドレスに、オペランドで指定されたオフセットを加えた位置にジャンプします。
goto と異なる点は、この jsr 命令の次の命令のアドレスを戻り番地としてスタックにプッシュする点です。
この命令は Java SE 6 以前では ret 命令と合わせて finally の実装に用いられていましたが、現在ではあまり使われていません。

ret (0xa9)

1バイトのオペランドを取ります。
オペランドで指定されたインデックスのローカル変数に格納された、戻り番地にジャンプします。
jsr はスタックに戻り番地をプッシュしますが、ret はローカル変数から戻り番地を得ます。この非対称性は意図的なものです。
wide 命令と組み合わせて用いることで、オペランドを 2 バイトに拡張できます。
この命令は Java SE 6 以前では jsr 命令と合わせて finally の実装に用いられていましたが、現在ではあまり使われていません。

tableswitch (0xaa)

可変長のオペランドを取ります。
まず、命令の直後に0から3バイト分のパディング(0埋め)が入ります。
これはオペランドの先頭が、命令列の先頭から4の倍数バイトになるように調整します。
次に、3つの4バイトの符号付きのオペランド(順に default, min, max とする)を取ります。
更にその直後に max - min + 1 個の4バイトの符号付きのオペランドを取ります。
これはジャンプテーブルとして機能します。
まとめると、以下の様な構造となっています。

tableswitch_instruction {
  tableswitch   // 0xaa
  padding       // 0-3 byte
  default       // 4 byte
  min           // 4 byte
  max           // 4 byte
  jumptable     // 4 byte * (max - min + 1) 個
}

この命令はスタックから1つのint値をポップし(index とする)、その値により分岐を行います。
index が min より小さいか max より大きい場合、
分岐先はこの命令のアドレスに default をオフセットとして加えた位置です。
そうでない場合、分岐先はこの命令のアドレスにジャンプテーブルの index - min 番目の値をオフセットとして加えた位置となります。

lookupswitch (0xab)

可変長のオペランドを取ります。
まず、命令の直後に0から3バイト分のパディング(0埋め)が入ります。
これはオペランドの先頭が、命令列の先頭から4の倍数バイトになるように調整します。
次に、2つの4バイトの符号付きのオペランド(順に default, count とする)
更にその直後に、2つの4バイトの符号付きの値(順に match, offset とする)の組が count 個続きます。
これはジャンプテーブルとして機能します。
まとめると、以下の様な構造となっています。

lookupswitch_instruction {
  lookupswitch  // 0xab
  padding       // 0-3 byte
  default       // 4 byte
  count         // 4 byte
  cases         // 8 byte * count 個, 各 case は lookupswitch_case
}
lookupswitch_case {
  match         // 4 byte
  offset        // 4 byte
}

この命令はスタックから1つのint値をポップし(index とする)、その値により分岐を行います。
index とジャンプテーブルの各エントリの match を比較し、一致するものが存在すれば、その match に対応する offset をこの命令のアドレスに加えた位置にジャンプします。
そうでなければ、この命令のアドレスに default をオフセットとして加えた位置にジャンプします。

復帰命令

ireturn (0xac), lreturn (0xad), freturn (0xae), dreturn (0xaf), areturn (0xb0)

スタックから1つの値をポップし、その値を呼び出し元のオペランドスタックにプッシュします。
現在のメソッドのオペランドスタックに残った値は全て捨てられます。
現在のメソッドが synchronized メソッドであった場合、このメソッドの呼び出しによりロックされたモニタはそのロックが開放されます。
この命令を実行後、処理系は呼び出し元のメソッドに処理を戻します。

return (0xb1)

現在のメソッドのオペランドスタックに残った値を全て捨てて、呼び出し元のメソッドに処理を戻します。
現在のメソッドが synchronized メソッドであった場合、このメソッドの呼び出しによりロックされたモニタはそのロックが開放されます。

フィールドアクセス命令

getstatic (0xb2)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、フィールドを表現する定数を指しています。
指定されたフィールドの値をスタックにプッシュします。
指定されたフィールドは static でなければなりません。

putstatic (0xb3)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、フィールドを表現する定数を指しています。
この命令はスタックから1つの値をポップし、その値をフィールドの値としてセットします。
セットされる値はフィールドの型と互換性のある型でなければなりません。
また、指定されたフィールドは static でなければなりません。

getfield (0xb4)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、フィールドを表現する定数を指しています。
この命令はスタックから1つのオブジェクト参照をポップし、そのオブジェクトの指定されたフィールドの値をスタックにプッシュします。
この命令では配列の length フィールドを取得することはできません。
配列の length フィールドを取得するには arraylength 命令を利用します。

putfield (0xb5)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、フィールドを表現する定数を指しています。
この命令はスタックから2つの値をポップし、1つ目(スタックの上にあったほう)を value 、2つ目をオブジェクト参照 ref として、 ref の指定されたフィールドに value の値をセットします。
セットされる値はフィールドの型と互換性のある型でなければなりません。

メソッド呼び出し命令

invokevirtual (0xb6)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、メソッドを表現する定数を指しています。
この命令は通常のメソッド呼び出しを表します。
スタックから引数リスト args とオブジェクト参照 ref をポップし、ref の指定されたメソッドを呼び出します。
args のほうが ref よりもスタックの上に格納されており、
args はインデックスが若い引数のほうがスタックの奥に格納されています。
つまり、スタックの状態は以下のようになっています。

..., ref, args[0], args[1], ..., args[n]

メソッドが native でない場合、新しくフレームが作成され、ref が0番目のローカル変数に、args が1番目以降のローカル変数に格納されます。
その後、呼び出したメソッドに処理が移されます。
この命令を実行後のスタックの状態は呼び出したメソッドの復帰命令により変わります。
メソッドが native のとき、対応する native 関数が呼ばれます。
メソッドが synchronized のとき、ref に対応するモニタがロックされます。

呼び出されるメソッドは以下の様な手続きにより探索されます。

  1. ref のクラスを C とします。
  2. C が指定されたメソッドをオーバーライドしたメソッド m を実装していたら、呼び出されるメソッドは m となり、探索は終了します。
  3. C が親クラスを持つ場合、C を直接の親クラスに置き換えて 2 に戻ります。
  4. メソッドが見つからなかった場合、AbstractMethodError を送出します。

invokespecial (0xb7)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、メソッドを表現する定数を指しています。
この命令はインスタンスの初期化、private メソッドの呼び出し、及び親クラスのメソッドの呼び出しに用いられます。
スタックから引数リスト args とオブジェクト参照 ref をポップし、ref の指定されたメソッドを呼び出します。
args のほうが ref よりもスタックの上に格納されており、
args はインデックスが若い引数のほうがスタックの奥に格納されています。
つまり、スタックの状態は以下のようになっています。

..., ref, args[0], args[1], ..., args[n]

メソッドが native でない場合、新しくフレームが作成され、ref が0番目のローカル変数に、args が1番目以降のローカル変数に格納されます。
その後、呼び出したメソッドに処理が移されます。
この命令を実行後のスタックの状態は呼び出したメソッドの復帰命令により変わります。
メソッドが native のとき、対応する native 関数が呼ばれます。
メソッドが synchronized のとき、ref に対応するモニタがロックされます。

この命令と invokevirtual の違いはメソッドの探索方法です。
呼び出されるメソッドは以下の様な手続きにより探索されます。

  1. 現在実行しているメソッドの属するクラスを Cur, 指定されたメソッド m のクラスを Cm とします。
  2. Cur に ACC_SUPER フラグが立っていて、Cm が Cur の親クラスで、m がインスタンスの初期化メソッド(メソッド名がであるメソッド)でない場合、4 以下を実行します。
  3. m を呼び出されるメソッドとして探索を終了します。
  4. C を Cur の直接の親クラスとします。
  5. C が m と同名で同じ型指定子を持つメソッドを実装していたら、そのメソッドを呼び出されるメソッドとして探索を終了します。
  6. C が親クラスを持つ場合、C を直接の親クラスに置き換えて 5 に戻ります。
  7. メソッドが見つからなかった場合、AbstractMethodError を送出します。

invokestatic (0xb8)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、メソッドを表現する定数を指しています。
この命令はスタティックメソッド呼び出しを表します。
スタックから引数リスト args をポップし、指定されたメソッドを呼び出します。
args はインデックスが若い引数のほうがスタックの奥に格納されています。
メソッドが native でない場合、新しくフレームが作成され、args が0番目以降のローカル変数に格納されます。
この命令を実行後のスタックの状態は呼び出したメソッドの復帰命令により変わります。
メソッドが native のとき、対応する native 関数が呼ばれます。
メソッドが synchronized のとき、メソッドの属するクラスのクラスオブジェクトに対応するモニタがロックされます。

invokeinterface (0xb9)

2バイトのオペランドと1バイトのオペランドをとり、またその直後の1バイトは0埋めされます。
最初の2バイトは定数プールのインデックスで、インターフェースメソッドを表現する定数を指しています。
次の1バイトは指定されたメソッドが持つ引数の数を表現しています。
この情報はメソッドの型指定子からも得ることができる情報で、この冗長性は歴史的な理由によるものです。
まとめると、以下の様な構造となっています。

invokeinterface_instruction {
  invokeinterface         // 0xb9
  interface_method_ref    // 2 byte
  count                   // 1 byte
  0                       // 1 byte
}

この命令はインターフェースメソッドの呼び出しを表現しています。
スタックから引数リスト args とオブジェクト参照 ref をポップし、ref の指定されたメソッドを呼び出します。
args のほうが ref よりもスタックの上に格納されており、
args はインデックスが若い引数のほうがスタックの奥に格納されています。
つまり、スタックの状態は以下のようになっています。

..., ref, args[0], args[1], ..., args[n]

メソッドが native でない場合、新しくフレームが作成され、ref が0番目のローカル変数に、args が1番目以降のローカル変数に格納されます。
その後、呼び出したメソッドに処理が移されます。
この命令を実行後のスタックの状態は呼び出したメソッドの復帰命令により変わります。
メソッドが native のとき、対応する native 関数が呼ばれます。
メソッドが synchronized のとき、ref に対応するモニタがロックされます。

呼び出されるメソッドは以下の様な手続きにより探索されます。

  1. ref のクラスを C とします。
  2. C が指定されたメソッドと同名で同じ型指定子を持つメソッドを実装していたら、そのメソッドを呼び出されるメソッドとして探索を終了します。
  3. C が親クラスを持つ場合、C を直接の親クラスに置き換えて 2 に戻ります。
  4. メソッドが見つからなかった場合、AbstractMethodError を送出します。

これはほとんど invokevirtual と同じように見えます。
(おそらく効率を無視すれば1つにまとめることも可能だと思います。)
しかし、このように分けることによって実行時の最適化などが行いやすくなるようです。

invokedynamic (0xba)

2バイトのオペランドを取り、またその直後の2バイトは0埋めされます。
2バイトのオペランドは定数プールのインデックスで、call site specifier と呼ばれる定数を指しています。
この命令はメソッドを動的に解決して呼び出すことを表します。
スタックから引数リスト args をポップし、指定されたメソッドを呼び出します。
invokedynamic は残念ながらよくわからないので、深入りは避けておきます。

インスタンス生成命令

new (0xbb)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、クラスを表現する定数を指しています。
ヒープ領域から指定されたクラスのインスタンスの分のメモリ領域が確保・初期化されます。
この初期化はデフォルト値による初期化で、コンストラクタは別途呼ぶ必要があります。
新しく作成されたインスタンスへの参照がスタックにプッシュされます。

newarray (0xbc)

1バイトのオペランドを取ります。
オペランドは配列の型を表現する定数で、値と型の対応は以下のとおりです。

4 boolean[] 8 byte[]
5 char[] 9 short[]
6 float[] 10 int[]
7 double[] 11 long[]

この命令はプリミティブ型の配列を新たに作成します。
スタックからint値をポップし、指定されたサイズの配列を作成します。
ヒープ領域上に必要な分のメモリ領域が確保・初期化されます。
作成された配列への参照はスタックにプッシュされます。

anewarray (0xbd)

2バイトのオペランドを取ります。
オペランドは定数プールのインデックスで、クラスを表現する定数を指しています。
この命令は指定された参照型の配列を新たに作成するものです。
スタックからint値をポップし、指定されたサイズの配列を作成します。
作成された配列への参照はスタックにプッシュされます。

配列長取得命令

arraylength (0xbe)

スタックから配列への参照をポップし、その配列の長さスタックにプッシュします。
配列の長さは int 値で表現されます。
配列の参照が null であった場合、NullPointerException がスローされます。

例外送出命令

athrow (0xbf)

スタックから例外を表現するオブジェクトへの参照をポップし、その例外を送出します。
例外オブジェクトが null であった場合、NullPointerException がスローされます。

実行時型チェック命令

checkcast (0xc0)

2バイトのオペランドをとります。
オペランドは定数プールのインデックスで、クラスを表現する定数を指しています。
スタックからオブジェクトへの参照をポップし、そのオブジェクトが指定されたクラスの型にキャストできるかをチェックします。
オブジェクト参照が null であるか、指定されたクラスの型にキャストできる場合は、そのオブジェクト参照をそのままスタックに戻します。
そうでない場合、ClassCastException を送出します。
この命令は instanceof 命令と非常に似ていますが、null の扱い方とスタックの操作が異なります。

instanceof (0xc1)

2バイトのオペランドをとります。
オペランドは定数プールのインデックスで、クラスを表現する定数を指しています。
スタックからオブジェクトへの参照をポップし、そのオブジェクトが指定されたクラスのインスタンスであるかをチェックします。
オブジェクト参照が null である場合 int 値 0 をスタックにプッシュします。
参照されたオブジェクトが指定されたクラスのインスタンスである場合 int 値 1 をスタックにプッシュします。
そうでない場合、スタックに int 値 0 をプッシュします。

同期用命令

monitorenter (0xc2)

この命令は synchronized のロックの取得を試みるものです。
スタックからオブジェクト参照をポップし、そのオブジェクトに関連付けられたモニタのロックの取得を行います。
モニタのエントリカウントが 0 であれば、このスレッドがロックを取得しエントリカウントを 1 にします。
すでにこのスレッドがロックを取得している場合、エントリカウントを 1 増加させます。
他のスレッドがロックをすでに取得している場合、このスレッドはブロックされ、エントリカウントが 0 になったら再度ロックの取得を試みます。
オブジェクト参照が null であった場合、NullPointerException がスローされます。

monitorexit (0xc3)

この命令は synchronized のロックの解放を行うものです。
スタックからオブジェクト参照をポップし、そのオブジェクトに関連付けられたモニタのロックの解放を行います。
モニタのエントリカウントを 1 減らし、エントリカウントが 0 になればロックを解放します。
オブジェクト参照が null であった場合、NullPointerException がスローされます。
このスレッドが指定されたオブジェクトに関連付けられたモニタのロックを取得していない場合、IllegalMonitorStateException がスローされます。

メタ命令

wide (0xc4)

この命令に続く命令のオペランド長を拡張する命令です。
iload, lload, fload, dload, aload, istore, lstore, fstore, dstore, astore, ret の各命令のオペランドを2バイトに拡張します。
また、iinc 命令の2つのオペランドをそれぞれ2バイトに拡張します。

その他便利命令

multianewarray (0xc5)

2バイトのオペランドと1バイトのオペランドをとります。
最初の2バイトのオペランドは定数プールのインデックスで、クラスを表現する定数を指しています。
次の1バイトは1以上の整数で、配列の次元を表現します。
この命令は指定された型の多次元配列を作成する命令です。
スタックから指定された次元の数だけ int 値をポップし、それを配列の長さとします。
例えば、スタックが以下のようになっていた場合、

..., l1, l2, ..., ln

l1 * l2 * ... * ln の n次元配列が作成されます。
作成された配列への参照はスタックにプッシュされます。
0以下の次元が指定された場合、NegativeArraySizeException がスローされます。

ifnull (0xc6)

2バイトの符号付きのオペランドを取ります。
スタックからオブジェクト参照をポップし、それが null であれば分岐をします。
分岐先はこの命令のアドレスに、オペランドで指定されたオフセットを加えた位置です。

ifnonnull (0xc7)

2バイトの符号付きのオペランドを取ります。
スタックからオブジェクト参照をポップし、それが null でなければ分岐をします。
分岐先はこの命令のアドレスに、オペランドで指定されたオフセットを加えた位置です。

goto_w (0xc8)

4バイトの符号付きのオペランドを取ります。
この命令のアドレスに、オペランドで指定されたオフセットを加えた位置にジャンプします。

jsr_w (0xc9)

4バイトの符号付きのオペランドを取ります。
この命令のアドレスに、オペランドで指定されたオフセットを加えた位置にジャンプします。
goto_w と異なる点は、この jsr 命令の次の命令のアドレスを戻り番地としてスタックにプッシュする点です。
この命令は Java SE 6 以前では ret 命令と合わせて finally の実装に用いられていましたが、現在ではあまり使われていません。

あとがき

我ながら大作です。
というか途中で面倒になって8割くらいできたところで投げ出してました。(ごめんなさい)
正直最初の方は何書いたかあまり覚えてないんですが、間違った点などあれば教えてください。
(指摘できる人はかなり少数だと思いますが)

(追記) jsr/ret命令について、現在ではほとんど使われていないという記述を追加

14
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
11