各スタックフレームには、ローカル変数テーブルに加えて、後入れ先出し(LIFO)の操作数スタック(オペランドスタック、式スタックとも呼ぶ)が含まれます。操作数スタックは、計算の中間結果を保存したり、計算中に変数の一時的なストレージとして機能します。
1. 操作数スタックの役割
JVMのインタプリタエンジンはスタックベースの実行エンジンと呼ばれ、この「スタック」は操作数スタックを指します。Javaのすべての命令は、実行される前にまずそのオペランドを操作数スタックにプッシュします。
一部のバイトコード命令は値をスタックにプッシュし、残りの命令はオペランドをポップして、コピー、交換、加算などの操作を行います。その後、結果を再びスタックにプッシュします。図4-19のように、2と3がそれぞれポップされ、iadd命令の実行後に結果がプッシュされます。
操作数スタックはJVM実行エンジンの作業領域であり、メソッドの実行が開始されると新しいスタックフレームが作成され、その操作数スタックは空です。操作数スタックへのデータアクセスはインデックスでは行えず、標準のプッシュ(push)とポップ(pop)操作のみで行われます。
呼び出されたメソッドが戻り値を持つ場合、その戻り値は現在のスタックフレームの操作数スタックにプッシュされ、プログラムカウンタは次に実行すべきバイトコード命令を指すように更新されます。操作数スタック内の要素のデータ型はバイトコード命令シーケンスと厳密に一致する必要があり、これはコンパイル時に検証され、さらにクラスロード時のデータフロー解析フェーズでも再度検証されます。
2. 操作数スタックのバイトコードデモ
以下のコード例を使用して動作を確認します。
public class OperandStackTest {
public void testAddOperation() {
byte i = 2;
int j = 3;
int k = (int)i + j;
}
}
javap -v コマンドでクラスファイルを逆コンパイルした結果の一部を以下に示します。
public void testAddOperation();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
バイトコードの各ステップの追跡は以下の通りです。
- iconst_2 命令:値2をbyte型からint型に変換し、操作数スタックのトップにプッシュします(byte、short、char型の値はプッシュ前にint型に変換されます)。
- istore_1 命令:スタックトップの値をポップし、ローカル変数テーブルのインデックス1に格納します。
- iconst_3 命令:値3をスタックトップにプッシュします。
- istore_2 命令:スタックトップの値をポップし、ローカル変数テーブルのインデックス2に格納します。
- iload_1 命令:ローカル変数テーブルのインデックス1から値2を再びスタックトップにプッシュします。
- iload_2 命令:ローカル変数テーブルのインデックス2から値3を再びスタックトップにプッシュします。
- iadd 命令:2つの値をポップして加算し、結果(5)をスタックトップにプッシュします。
- istore_3 命令:計算結果をポップし、ローカル変数テーブルのインデックス3に格納します。return命令でメソッドを終了します。
図4-20から4-27は上記の各ステップを示しています。