バイトコードの基本

「UnderTheHood」の別の記事へようこそ。このコラムでは、Java開発者に、実行中のJavaプログラムの下で何が起こっているかを垣間見ることができます。今月の記事では、Java仮想マシン(JVM)のバイトコード命令セットについて最初に説明します。この記事では、バイトコードによって操作されるプリミティブ型、型間で変換されるバイトコード、およびスタック上で動作するバイトコードについて説明します。以降の記事では、バイトコードファミリの他のメンバーについて説明します。

バイトコード形式

バイトコードは、Java仮想マシンの機械語です。 JVMがクラスファイルをロードすると、クラス内のメソッドごとに1つのバイトコードストリームを取得します。バイトコードストリームは、JVMのメソッド領域に格納されます。メソッドのバイトコードは、プログラムの実行中にそのメソッドが呼び出されたときに実行されます。これらは、解釈、ジャストインタイムコンパイル、または特定のJVMの設計者が選択したその他の手法によって実行できます。

メソッドのバイトコードストリームは、Java仮想マシンの一連の命令です。各命令は、1バイトのオペコードとそれに続く0個以上のオペランドで構成されます。オペコードは、実行するアクションを示します。JVMがアクションを実行する前にさらに情報が必要な場合、その情報は、オペコードの直後に続く1つ以上のオペランドにエンコードされます。

各タイプのオペコードにはニーモニックがあります。典型的なアセンブリ言語スタイルでは、Javaバイトコードのストリームは、ニーモニックとそれに続く任意のオペランド値で表すことができます。たとえば、次のバイトコードのストリームをニーモニックに分解できます。

//バイトコードストリーム:03 3b 84 00 01 1a 05 68 3b a7 ff f9 //逆アセンブリ:iconst_0 // 03 istore_0 // 3b iinc 0、1 // 84 00 01 iload_0 // 1a iconst_2 // 05 imul // 68 istore_0 // 3b goto -7 // a7 ff f9 

バイトコード命令セットは、コンパクトになるように設計されています。テーブルジャンプを処理する2つを除くすべての命令は、バイト境界に配置されます。オペコードの総数は十分に少ないため、オペコードは1バイトしか占有しません。これにより、JVMによってロードされる前にネットワーク間を移動する可能性のあるクラスファイルのサイズを最小限に抑えることができます。また、JVM実装のサイズを小さく保つのにも役立ちます。

JVMでのすべての計算は、スタックを中心にしています。 JVMには任意の値を格納するためのレジスタがないため、計算で使用する前に、すべてをスタックにプッシュする必要があります。したがって、バイトコード命令は主にスタック上で動作します。たとえば、上記のバイトコードシーケンスでは、最初にローカル変数をiload_0命令でスタックにプッシュし、次に2をスタックにプッシュすることでローカル変数に2を掛けますiconst_2。両方の整数がスタックにプッシュされた後、imul命令は2つの整数をスタックから効果的にポップし、それらを乗算して、結果をスタックにプッシュします。結果はスタックの一番上からポップされ、ローカル変数に格納されます。istore_0命令。JVMは、Intel 486などのレジスタの少ないアーキテクチャでの効率的な実装を容易にするために、レジスタベースのマシンではなくスタックベースのマシンとして設計されました。

プリミティブ型

JVMは、7つのプリミティブデータ型をサポートします。Javaプログラマーは、これらのデータ型の変数を宣言して使用でき、Javaバイトコードはこれらのデータ型を操作します。次の表に、7つのプリミティブ型を示します。

タイプ 定義
byte 1バイトの符号付き2の補数整数
short 2バイト符号付き2の補数整数
int 4バイトの符号付き2の補数整数
long 8バイトの符号付き2の補数整数
float 4バイトIEEE754単精度浮動小数点数
double 8バイトIEEE754倍精度浮動小数点数
char 2バイトの符号なしUnicode文字

プリミティブ型は、バイトコードストリームのオペランドとして表示されます。1バイトを超えるすべてのプリミティブ型は、バイトコードストリームにビッグエンディアンの順序で格納されます。つまり、上位バイトが下位バイトよりも優先されます。たとえば、定数値256(16進0100)をスタックにプッシュするには、sipushオペコードの後に​​短いオペランドを使用します。JVMはビッグエンディアンであるため、ショートは以下に示すバイトコードストリームに「0100」として表示されます。JVMがリトルエンディアンの場合、ショートは「0001」と表示されます。

//バイトコードストリーム:17 01 00 //分解:sipush 256; // 17 01 00

Javaオペコードは通常、オペランドのタイプを示します。これにより、JVMに対してオペランドのタイプを識別する必要がなく、オペランドをそれ自体にすることができます。たとえば、ローカル変数をスタックにプッシュする1つのオペコードではなく、JVMには複数のオペコードがあります。オペコードはiloadlloadfload、およびdloadスタック上に、それぞれのタイプのint、long、float型、ダブル、のローカル変数を押してください。

定数をスタックにプッシュする

多くのオペコードは定数をスタックにプッシュします。オペコードは、3つの異なる方法でプッシュする定数値を示します。定数値は、オペコード自体に暗黙的に含まれるか、バイトコードストリーム内のオペコードにオペランドとして続くか、定数プールから取得されます。

一部のオペコードは、それ自体で、プッシュするタイプと定数値を示します。たとえば、iconst_1オペコードはJVMに整数値1をプッシュするように指示します。このようなバイトコードは、さまざまなタイプの一般的にプッシュされるいくつかの数値に対して定義されています。これらの命令は、バイトコードストリームで1バイトしか占有しません。これらはバイトコード実行の効率を高め、バイトコードストリームのサイズを減らします。intとfloatをプッシュするオペコードを次の表に示します。

オペコード オペランド 説明
iconst_m1 (なし) int-1をスタックにプッシュします
iconst_0 (なし) int0をスタックにプッシュします
iconst_1 (なし) int1をスタックにプッシュします
iconst_2 (なし) int2をスタックにプッシュします
iconst_3 (なし) int3をスタックにプッシュします
iconst_4 (なし) int4をスタックにプッシュします
iconst_5 (なし) int5をスタックにプッシュします
fconst_0 (なし) float0をスタックにプッシュします
fconst_1 (なし) フロート1をスタックにプッシュします
fconst_2 (なし) フロート2をスタックにプッシュします

The opcodes shown in the previous table push ints and floats, which are 32-bit values. Each slot on the Java stack is 32 bits wide. Therefore each time an int or float is pushed onto the stack, it occupies one slot.

The opcodes shown in the next table push longs and doubles. Long and double values occupy 64 bits. Each time a long or double is pushed onto the stack, its value occupies two slots on the stack. Opcodes that indicate a specific long or double value to push are shown in the following table:

Opcode Operand(s) Description
lconst_0 (none) pushes long 0 onto the stack
lconst_1 (none) pushes long 1 onto the stack
dconst_0 (none) pushes double 0 onto the stack
dconst_1 (none) pushes double 1 onto the stack

One other opcode pushes an implicit constant value onto the stack. The aconst_null opcode, shown in the following table, pushes a null object reference onto the stack. The format of an object reference depends upon the JVM implementation. An object reference will somehow refer to a Java object on the garbage-collected heap. A null object reference indicates an object reference variable does not currently refer to any valid object. The aconst_null opcode is used in the process of assigning null to an object reference variable.

Opcode Operand(s) Description
aconst_null (none) pushes a null object reference onto the stack

Two opcodes indicate the constant to push with an operand that immediately follows the opcode. These opcodes, shown in the following table, are used to push integer constants that are within the valid range for byte or short types. The byte or short that follows the opcode is expanded to an int before it is pushed onto the stack, because every slot on the Java stack is 32 bits wide. Operations on bytes and shorts that have been pushed onto the stack are actually done on their int equivalents.

Opcode Operand(s) Description
bipush byte1 expands byte1 (a byte type) to an int and pushes it onto the stack
sipush byte1, byte2 expands byte1, byte2 (a short type) to an int and pushes it onto the stack

Three opcodes push constants from the constant pool. All constants associated with a class, such as final variables values, are stored in the class's constant pool. Opcodes that push constants from the constant pool have operands that indicate which constant to push by specifying a constant pool index. The Java virtual machine will look up the constant given the index, determine the constant's type, and push it onto the stack.

The constant pool index is an unsigned value that immediately follows the opcode in the bytecode stream. Opcodes lcd1 and lcd2 push a 32-bit item onto the stack, such as an int or float. The difference between lcd1 and lcd2 is that lcd1 can only refer to constant pool locations one through 255 because its index is just 1 byte. (Constant pool location zero is unused.) lcd2 has a 2-byte index, so it can refer to any constant pool location. lcd2w also has a 2-byte index, and it is used to refer to any constant pool location containing a long or double, which occupy 64 bits. The opcodes that push constants from the constant pool are shown in the following table:

Opcode Operand(s) Description
ldc1 indexbyte1 pushes 32-bit constant_pool entry specified by indexbyte1 onto the stack
ldc2 indexbyte1, indexbyte2 pushes 32-bit constant_pool entry specified by indexbyte1, indexbyte2 onto the stack
ldc2w indexbyte1, indexbyte2 pushes 64-bit constant_pool entry specified by indexbyte1, indexbyte2 onto the stack

Pushing local variables onto the stack

Local variables are stored in a special section of the stack frame. The stack frame is the portion of the stack being used by the currently executing method. Each stack frame consists of three sections -- the local variables, the execution environment, and the operand stack. Pushing a local variable onto the stack actually involves moving a value from the local variables section of the stack frame to the operand section. The operand section of the currently executing method is always the top of the stack, so pushing a value onto the operand section of the current stack frame is the same as pushing a value onto the top of the stack.

The Java stack is a last-in, first-out stack of 32-bit slots. Because each slot in the stack occupies 32 bits, all local variables occupy at least 32 bits. Local variables of type long and double, which are 64-bit quantities, occupy two slots on the stack. Local variables of type byte or short are stored as local variables of type int, but with a value that is valid for the smaller type. For example, an int local variable which represents a byte type will always contain a value valid for a byte (-128 <= value <= 127).

Each local variable of a method has a unique index. The local variable section of a method's stack frame can be thought of as an array of 32-bit slots, each one addressable by the array index. Local variables of type long or double, which occupy two slots, are referred to by the lower of the two slot indexes. For example, a double that occupies slots two and three would be referred to by an index of two.

Several opcodes exist that push int and float local variables onto the operand stack. Some opcodes are defined that implicitly refer to a commonly used local variable position. For example, iload_0 loads the int local variable at position zero. Other local variables are pushed onto the stack by an opcode that takes the local variable index from the first byte following the opcode. The iload instruction is an example of this type of opcode. The first byte following iload is interpreted as an unsigned 8-bit index that refers to a local variable.

Unsigned 8-bit local variable indexes, such as the one that follows the iload instruction, limit the number of local variables in a method to 256. A separate instruction, called wide, can extend an 8-bit index by another 8 bits. This raises the local variable limit to 64 kilobytes. The wide opcode is followed by an 8-bit operand. The wide opcode and its operand can precede an instruction, such as iload, that takes an 8-bit unsigned local variable index. The JVM combines the 8-bit operand of the wide instruction with the 8-bit operand of the iload instruction to yield a 16-bit unsigned local variable index.

The opcodes that push int and float local variables onto the stack are shown in the following table:

Opcode Operand(s) Description
iload vindex pushes int from local variable position vindex
iload_0 (none) intをローカル変数の位置0からプッシュします
iload_1 (なし) intをローカル変数の位置1からプッシュします
iload_2 (なし) intをローカル変数の位置2からプッシュします
iload_3 (なし) intをローカル変数の位置3からプッシュします
fload vindex ローカル変数の位置vindexからfloatをプッシュします
fload_0 (なし) ローカル変数の位置ゼロからフロートをプッシュします
fload_1 (なし) ローカル変数の位置1からフロートをプッシュします
fload_2 (なし) ローカル変数の位置2からフロートをプッシュします
fload_3 (なし) ローカル変数の位置3からフロートをプッシュします

次の表は、long型とdouble型のローカル変数をスタックにプッシュする命令を示しています。これらの命令は、スタックフレームのローカル変数セクションからオペランドセクションに64ビットを移動します。