This article focuses on bytecode instructions, providing an in-depth yet accessible explanation of various bytecode instruction types, including: loading and storing, arithmetic operations, type conversion, object creation and access, method invocation and return, control flow, exception handling, and synchronization.
Due to the extensive variety of bytecode instructions, this article serves as the first part, covering loading and storing, arithmetic, and type conversion instructions.
Use the jclasslib plugin in IDEA to view compiled bytecode instructions.
Bytecode Instruction Set
Most instructions begin with prefixes like i (int), l (long), f (float), d (double), a (reference).
Note: byte, char, short, and boolean types are converted to int in HotSpot and executed using int-type bytecode instructions.
Bytecode instructions are generally categorized into:
- Loading and Storing Instructions
- Arithmetic Instructions
- Type Conversion Instructions
- Object Creation and Access Instructions
- Method Invocation and Return Instructions
- Operand Stack Management Instructions
- Control Flow Instructions
- Exception Handling Instructions
- Synchronization Control Instructions
In HotSpot, each method corresponds to a set of bytecode instructions.
These bytecode instructions operate on the local variable table and operand stack within the method's corresponding stack frame.
Bytecode instructions consist of bytecode operation codes and operands (operands may come from the local variable table, constant pool, or be constants).
Loading and Storing Instructions
Loading
Loading instructions push operands onto the operand stack (from the local variable table or constant pool).
-
Local Variable Table Loading Instructions
i/l/f/d/aloadfollowed by the operand indicates which slot in the local variable table to retrieve the value fromiload_0: Retrieve the int value from slot 0 of the local variable table
-
Constant Loading Instructions
- Can be categorized into three types based on the range of constants loaded:
const < push < ldc
- Can be categorized into three types based on the range of constants loaded:
Storing
Storing instructions pop the top element from the operand stack and store it in a specific slot of the local variable table.
-
Storing Instructions
i/l/f/d/astorefollowed by the operand indicates which slot in the local variable table to store the value inistore_1: Pop the top int element from the stack and store it in slot 1 of the local variable table
Note: At compile time, the number of slots required in the local variable table and the maximum depth of the operand stack are determined (to save space, local variable slots may be reused).
Load 100 from the constant pool and store it in slot 1 of the local variable table, load 200 from the constant pool and store it in slot 2 of the local variable table (where slot 0 of the local variable table stores this).
Arithmetic Instructions
Arithmetic instructions pop two top elements from the operand stack, perform an operation, and push the result back onto the stack.
They use postfix notation (Reverse Polish notation), for example: 3 4 + => 3 + 4.
Notes
- When the divisor is 0, an ArithmeticException is thrown
- Floating-point to integer conversion truncates toward zero
- Floating-point calculations may lose precision
- Infinity results in infinite values
- NaN results in undefined calculation values
public void testArithmetic() {
double d1 = 10 / 0.0;
// Infinity
System.out.println(d1);
double d2 = 0.0 / 0.0;
// NaN
System.out.println(d2);
// Truncation toward zero: floating-point to integer conversion
// 5
System.out.println((int) 5.9);
// -5
System.out.println((int) -5.9);
// Rounding to nearest: floating-point arithmetic
// 0.060000000000000005
System.out.println(0.05 + 0.01);
// Throws ArithmeticException: / by zero
System.out.println(1 / 0);
}
Type Conversion Instructions
Type conversion instructions can be divided into widening type conversions and narrowing type conversions (corresponding to implicit and explicit conversions of primitive types).
Widening Type Conversion
Conversion from smaller to larger ranges
-
int -> long -> float -> double
i2l,i2f,i2dl2f,l2df2d
byte, short, char use int-type instructions.
Note: When converting long to float or double, precision loss may occur.
public void testWideningConversion() {
long l1 = 123412345L;
long l2 = 1234567891234567899L;
float f1 = l1;
// Result: 1.23412344E8 => 123412344
// l1 = 123412345L
System.out.println(f1);
double d1 = l2;
// Result: 1.23456789123456794E18 => 1234567891234567940
// l2 = 1234567891234567899L
System.out.println(d1);
}
Narrowing Type Conversion
Conversion from larger to smaller ranges
- int -> byte, char, short:
i2b,i2c,i2s - long -> int:
l2i - float -> long, int:
f2l,f2i - double -> float, long, int:
d2f,d2l,d2i
If long, float, or double need to be converted to byte, char, or short, they can first be converted to int and then to the corresponding type.
Narrowing type conversion may result in precision loss.
Special cases for NaN and Infinity:
public void testNarrowingConversion() {
double d1 = Double.NaN;
double d2 = Double.POSITIVE_INFINITY;
int i1 = (int) d1;
int i2 = (int) d2;
// 0
System.out.println(i1);
// true
System.out.println(i2 == Integer.MAX_VALUE);
long l1 = (long) d1;
long l2 = (long) d2;
// 0
System.out.println(l1);
// true
System.out.println(l2 == Long.MAX_VALUE);
float f1 = (float) d1;
float f2 = (float) d2;
// NaN
System.out.println(f1);
// Infinity
System.out.println(f2);
}
Converting NaN to an integer results in 0.
Converting positive or negative infinity to an integer results in the maximum or minimum value of that type.
Object Creation and Access Instructions
Object creation and access instructions include: creation instructions, field access instructions, array manipulation instructions, and type checking instructions.
Creation Instructions
new: Create an instance
newarray: Create a one-dimensional array of primitive types
anewarray: Create a one-dimensional array of reference types
multianewarray: Create multi-dimensional arrays
Note: Creation here can be understood as memory allocation. When only one dimension of a multi-dimensional array is allocated, anewarray is used.
Field Access Instructions
getstatic: Read operation on static fields
putstatic: Write operation on static fields
getfield: Read operation on instance fields
putfield: Write operation on instance fields
Read operations: Push the field to be read onto the stack.
Write operations: Pop the value to be written from the stack and write it to the corresponding field.
Array Manipulation Instructions
-
b/c/s/i/l/f/d/a aload: Indicates pushing an element from a specific index in the array onto the stack (read)- Required parameters from top to bottom of the stack: index position, array reference
-
b/c/s/i/l/f/d/a astore: Indicates popping a value and writing it to a specific index in the array (write)- Required parameters from top to bottom of the stack: value to be written, index position, array reference
Note: Instructions starting with b are applicable to both byte and boolean.
arraylength: First pops the array reference, then pushes the obtained array length onto the stack
Type Checking Instructions
instanceof: Determine if an object is an instance of a specific class
checkcast: Check if a reference type can be cast
Summary
Due to the large number and length of bytecode instructions, they will be divided into two parts for in-depth explanation. This article, as the first part, provides an in-depth explanation of bytecode instruction introduction, loading and storing instructions, arithmetic instructions, type conversion instructions, and object creation and access instructions.
Most bytecode instructions start with i, l, f, d, a, corresponding to int, long, float, double, and reference respectively, where byte, char, short, and boolean are converted to int for execution.
Bytecode instructions consist of bytecode operation codes and required operands, which may come from the local variable table or constant pool.
Loading instructions load data from the local variable table or constant pool, while storing instructions store data into corresponding slots of the local variable table. The 0th slot of the local variable table for instance methods is commonly used to store this, and slots may be reused if variables are local to the method.
Arithmetic instructions provide arithmetic rules for various types and operations, using postfix notation on the operand stack.
Type conversion is divided into widening and narrowing, both of which may result in precision loss.
Object creation and access instructions include creating objects, accessing instance and static fields, manipulating arrays, and type checking instructions.