Bytecode basics

A first look at the bytecodes of the Java virtual machine

Welcome to another installment of "Under The Hood." This column gives Java developers a glimpse of what is going on beneath their running Java programs. This month's article takes an initial look at the bytecode instruction set of the Java virtual machine (JVM). The article covers primitive types operated upon by bytecodes, bytecodes that convert between types, and bytecodes that operate on the stack. Subsequent articles will discuss other members of the bytecode family.

The bytecode format

Bytecodes are the machine language of the Java virtual machine. When a JVM loads a class file, it gets one stream of bytecodes for each method in the class. The bytecodes streams are stored in the method area of the JVM. The bytecodes for a method are executed when that method is invoked during the course of running the program. They can be executed by intepretation, just-in-time compiling, or any other technique that was chosen by the designer of a particular JVM.

A method's bytecode stream is a sequence of instructions for the Java virtual machine. Each instruction consists of a one-byte opcode followed by zero or more operands. The opcode indicates the action to take. If more information is required before the JVM can take the action, that information is encoded into one or more operands that immediately follow the opcode.

Each type of opcode has a mnemonic. In the typical assembly language style, streams of Java bytecodes can be represented by their mnemonics followed by any operand values. For example, the following stream of bytecodes can be disassembled into mnemonics:

// Bytecode stream: 03 3b 84 00 01 1a 05 68 3b a7 ff f9
// Disassembly:
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

The bytecode instruction set was designed to be compact. All instructions, except two that deal with table jumping, are aligned on byte boundaries. The total number of opcodes is small enough so that opcodes occupy only one byte. This helps minimize the size of class files that may be traveling across networks before being loaded by a JVM. It also helps keep the size of the JVM implementation small.

All computation in the JVM centers on the stack. Because the JVM has no registers for storing abitrary values, everything must be pushed onto the stack before it can be used in a calculation. Bytecode instructions therefore operate primarily on the stack. For example, in the above bytecode sequence a local variable is multiplied by two by first pushing the local variable onto the stack with the iload_0 instruction, then pushing two onto the stack with iconst_2. After both integers have been pushed onto the stack, the imul instruction effectively pops the two integers off the stack, multiplies them, and pushes the result back onto the stack. The result is popped off the top of the stack and stored back to the local variable by the istore_0 instruction. The JVM was designed as a stack-based machine rather than a register-based machine to facilitate efficient implementation on register-poor architectures such as the Intel 486.

Primitive types

The JVM supports seven primitive data types. Java programmers can declare and use variables of these data types, and Java bytecodes operate upon these data types. The seven primitive types are listed in the following table:

TypeDefinition
byteone-byte signed two's complement integer
shorttwo-byte signed two's complement integer
int4-byte signed two's complement integer
long8-byte signed two's complement integer
float4-byte IEEE 754 single-precision float
double8-byte IEEE 754 double-precision float
char2-byte unsigned Unicode character

The primitive types appear as operands in bytecode streams. All primitive types that occupy more than 1 byte are stored in big-endian order in the bytecode stream, which means higher-order bytes precede lower-order bytes. For example, to push the constant value 256 (hex 0100) onto the stack, you would use the sipush opcode followed by a short operand. The short appears in the bytecode stream, shown below, as "01 00" because the JVM is big-endian. If the JVM were little-endian, the short would appear as "00 01".

 // Bytecode stream: 17 01 00
// Dissassembly:
sipush 256;      // 17 01 00

Java opcodes generally indicate the type of their operands. This allows operands to just be themselves, with no need to identify their type to the JVM. For example, instead of having one opcode that pushes a local variable onto the stack, the JVM has several. Opcodes iload, lload, fload, and dload push local variables of type int, long, float, and double, respectively, onto the stack.

Pushing constants onto the stack

Many opcodes push constants onto the stack. Opcodes indicate the constant value to push in three different ways. The constant value is either implicit in the opcode itself, follows the opcode in the bytecode stream as an operand, or is taken from the constant pool.

Some opcodes by themselves indicate a type and constant value to push. For example, the iconst_1 opcode tells the JVM to push integer value one. Such bytecodes are defined for some commonly pushed numbers of various types. These instructions occupy only 1 byte in the bytecode stream. They increase the efficiency of bytecode execution and reduce the size of bytecode streams. The opcodes that push ints and floats are shown in the following table:

OpcodeOperand(s)Description
iconst_m1(none)pushes int -1 onto the stack
iconst_0(none)pushes int 0 onto the stack
iconst_1(none)pushes int 1 onto the stack
iconst_2(none)pushes int 2 onto the stack
iconst_3(none)pushes int 3 onto the stack
iconst_4(none)pushes int 4 onto the stack
iconst_5(none)pushes int 5 onto the stack
fconst_0(none)pushes float 0 onto the stack
fconst_1(none)pushes float 1 onto the stack
fconst_2(none)pushes float 2 onto the stack

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:

OpcodeOperand(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.

OpcodeOperand(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.

OpcodeOperand(s)Description
bipushbyte1expands byte1 (a byte type) to an int and pushes it onto the stack
sipushbyte1, byte2expands 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:

OpcodeOperand(s)Description
ldc1indexbyte1pushes 32-bit constant_pool entry specified by indexbyte1 onto the stack
ldc2indexbyte1, indexbyte2pushes 32-bit constant_pool entry specified by indexbyte1, indexbyte2 onto the stack
ldc2windexbyte1, indexbyte2pushes 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:

OpcodeOperand(s)Description
iloadvindexpushes int from local variable position vindex
iload_0(none)pushes int from local variable position zero
iload_1(none)pushes int from local variable position one
iload_2(none)pushes int from local variable position two
iload_3(none)pushes int from local variable position three
floadvindexpushes float from local variable position vindex
fload_0(none)pushes float from local variable position zero
fload_1(none)pushes float from local variable position one
fload_2(none)pushes float from local variable position two
fload_3(none)pushes float from local variable position three

The next table shows the instructions that push local variables of type long and double onto the stack. These instructions move 64 bits from the local variable section of the stack frame to the operand section.

1 2 Page 1
Page 1 of 2
InfoWorld Technology of the Year Awards 2023. Now open for entries!