Initialization of virtual machine objects is done using a new instruction, which is called a bytecode instruction. JVM bytecode is a language that the JVM can recognize directly, and understand the file structure of bytecode files. Next, let’s learn more about bytecode instructions.
Let’s start with a simple program:
public class Main {
public static void main(String[] args) {
int x=3,y=2;
intr=x+y; System.out.println(x+y); }}Copy the code
Javap -c -s -v -l main.class: javap -c -s -v -l main.class
Let’s find out where the add instruction is:
For most data type-related bytecode instructions, there are special characters in their opcode mnemonics to indicate which data type is served specifically: I for int, L for long, S for short, B for byte, C for char, f for float, D for double, and A for reference.
Because the Java virtual machine operation code length is only one byte, so operation code contains the data types were brought a lot of pressure for the design of instruction set: if each instruction related to data type support Java virtual machine (all the runtime data type, then the number of orders I’m afraid I will be beyond the scope of the number of bytes can said. As a result, Java bytecode instructions support a limited number of pits for data types, and unsupported intelligent variants are handled by the supported bytecode instructions.
The JVM mainly supports byte, short, int, long, float, double, char, and reference data types. The opcodes of each data type start with a different letter. For example, iadd stands for the addition instruction code of int:
Next, let’s look at the different types of bytecode instructions.
1. Load and store instructions
Load and store instructions are used to transfer data back and forth between the local variable table and operand stack in a stack frame:
The main directives of this type are:
-
Load a local variable onto the stack: ILoad, ILoAD_ < N >, lload, lload_< N >, fload, fload_, dload, dload_, aload, ALOad_ < N >
-
Store a value from the operand stack to the local variable table: istore, istore_< N >, lstore, lstore_< N >, fstore, fstore_, dstore, dstore_< N >, astore, astore_< N >
-
Add a constant to the operand stack: bipush, sipush, LDC, LDC_w, LDC2_W, aconST_NULL, iconST_M1, iconst_< I >, LCONST_ < L >, fCONST_ <f>, dCONST_ <d>
-
Instruction that extends the access index of a local variable table: wide
The operand stack and local table of variables that store data are operated primarily by load and store instructions, but a few instructions, such as those that access fields of objects or array elements, also transfer data to the operand stack.
Iload_, a class of instructions ending in Angle brackets, actually represents a set of instructions, such as ILoad_, which may represent iload_0, ILOAD_1, ILoad_2, and iload_3, which represent loading the first, second, and third local variables into the operand stack.
2. Operational instructions
Arithmetic instructions are used to perform a specific operation on the values on two operand stacks and to store the results back to the top of the stack. Basically, there are two kinds of instructions: those that operate on integer data and those that operate on floating-point data.
All arithmetic instructions include:
-
Add instructions: iadd, ladd, fadd, dadd
-
Subtraction instructions: ISub, LSUB, fsub, dsub
-
Multiplication instruction: IMul, LMUl, FMUl, dMUl
-
Division instructions: IDIV, Ldiv, fdiv, ddiv
-
Redundant instructions: IREM, LREM, frem, DREM
-
Fetch counter instruction: ineG, Lneg, fNEg, dNEg
-
Displacement commands: ISHL, ISHR, IUSHR, LSHL, LSHR, LUShr
-
Bitwise or instruction: IOR, LOR
-
Bit and instruction: IAND, LAND
-
Xor instruction by bit: IXOR, LXOR
-
Local variable increment instruction: iinc
-
Comparison commands: DCMPG, DCMPL, FCMPG, FCMPL, LCMP
Type conversion instruction
Conversion instructions can convert two different numeric types to each other. These conversion operations have two functions:
- Display type operation conversion
- Type conversions not supported by bytecode instructions
There are two main types of conversion instructions:
Int — >long — >float — >double, i2l, i2f, i2D, L2F, l2d, f2d.
- There is no loss of precision from int to long, or int to double;
- Loss of precision can occur from int, long to float, or long to double;
- Wide-type conversions from byte, char, and short to int actually occur implicitly, thus reducing the number of bytecode instructions, which are, after all, only 256 for one byte.
2) narrow, convert a large type to a small type, such as int to byte, short, or char, corresponding instructions are: i2b, i2s, i2c; From long to int, the corresponding instructions are: l2I; From float to int or long, f2i, f2l; From double to int, long, or float, the corresponding instructions are d2i, d2l, d2f.
- Narrowing is likely to result in loss of precision, which is of a different order of magnitude;
- The Java virtual machine does not throw a runtime exception because of this.
3. Object creation and access instructions
We’ve already touched on the object creation directive.
The AVA virtual machine uses different bytecode instructions to create and manipulate class instances and arrays. After the object is created, fields or array elements in the object instance or array instance can be obtained by object access instructions. These instructions include:
-
Directive to create class instances: new
-
Instructions for creating arrays: newarray, anewarray, multianewarray
-
Directives that access class fields (static fields, or class variables) and instance fields (non-static fields, or instance variables) : getField, putfield, getStatic, putStatic
-
The instruction to load an array element into the operand stack: baload, caload, Saload, iaload, laload, faload, daload, aaload
-
Instructions to store the values of an operand stack in an array element: Bastore, Castore, sastore, iastore, fastore, dastore, aastore
-
The instruction to take the length of an array: arrayLength
-
Directives to check class instance types: instanceof, checkcast
4. Operand stack management instructions
As with the stack in a normal data structure, the Java virtual machine provides instructions for manipulating the operand stack directly, including:
-
Remove one or two elements from the top of the operand stack: pop, POP2
-
Duplicates one or two values from the top of the stack and pushes the duplicates or double duplicates back to the top: dUP, DUP2, DUp_X1, DUp2_X1, DUp_x2, dup2_x2
-
Swap the top two values of the stack: swap
5. Control transfer instruction
A control transfer instruction allows the Java virtual machine to conditionally or unconditionally proceed from the next instruction at a specified location (instead of the control transfer instruction). From a conceptual model, a control instruction can be thought of as modifying the value of a PC register conditionally or unconditionally.
Control transfer instructions include:
-
Conditional branch: Ifeq, IFLT, IFLE, IFNE, IFGT, IFGE, IFNULL, IFnonNULL, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, If_icmpgt, IF_ICmPLE, if_ICMPGE, if_ACMPEq, and if_ACMPne
-
Compound condition branches: Tableswitch and LookupSwitch
-
Unconditional branches: GOTO, GOTO_W, JSR, jSR_W, ret
In the Java virtual machine, there is a special set of instructions to handle the conditional branch comparison of int and reference types. There are also special instructions to detect null values without having to explicitly identify whether a data value is null.
Method calls and return directives
Method calls will be covered later, but here we just look at some of the instructions for method calls:
-
The Invokevirtual directive: An instance method used to invoke an object is dispatched based on the actual type of the object (virtual method dispatch), which is the most common method dispatch method in the Java language.
-
Invokeinterface directive: Invokes an interface method, which searches at run time for an object that implements the interface method to find an appropriate method to invoke.
-
The Invokespecial directive is used to call instance methods that require special processing, including instance initialization methods, private methods, and parent methods.
-
Invokestatic directive: Used to invoke class static methods (static methods).
-
Invokedynamic directive: Used to dynamically resolve the method referenced by the call point qualifier at run time. And execute the method. The dispatch logic for the first four invocation instructions is fixed inside the Java virtual machine and cannot be changed by the user, whereas the dispatch logic for the InvokeDynamic instruction is determined by the bootstrapped method that the user sets.
Method call directives are independent of the data type, while method return directives are differentiated by the type of value returned, including iReturn (used when the return value is Boolean, byte, CHAR, short, and int), LReturn, freturn, dreturn, and Areturn, There is also a return directive for methods declared as void, instance initializers, and class initializers of classes and interfaces.
7. Exception handling instructions
In addition to throwing an exception explicitly in a Java program (athrow statement), the Java virtual machine specification specifies that many runtime exceptions are automatically thrown when other Java virtual machine instructions detect exception conditions. For example, when the divisor is zero, the virtual machine throws the ArithmeticException exception in the IDIV or LDIV directive.
In the Java virtual machine, catch statements are handled not by bytecode instructions (JSR and RET instructions were used a long time ago, but are no longer used), but by exception tables.
8. Synchronization instruction
The Java virtual machine can support method-level synchronization and synchronization of a sequence of instructions within a method, both of which are implemented using a pipe procedure (Monitor, more commonly referred to as a “lock”).
Method-level synchronization is implicit and does not need to be controlled by bytecode instructions. It is implemented in method calls and return operations. The virtual machine can tell if a method is declared to be synchronized from the ACC_SYNCHRONIZED access flag in the method table structure in the method constant pool. When a method is invoked, the calling instruction checks to see if the ACC_SYNCHRONIZED access flag of the method is set. If so, the thread of execution requires that it successfully hold the pipe before executing the method, and finally release the pipe when the method completes (either normally or abnormally). During method execution, the executing thread holds the pipe, and no other thread can retrieve the same pipe. If a synchronized method throws an exception during execution and cannot handle the exception inside the method, the pipe held by the synchronized method is automatically released when the exception is thrown outside the synchronized method boundary.
Synchronizing a sequence of instructions is usually represented by the Synchronized statement block in Java language. The Java VIRTUAL machine has monitorenter and Monitorexit directives to support the semantics of synchronized. Proper implementation of the synchronized keyword requires the cooperation of both the Javac compiler and the Java virtual machine.
For example:
void onlyMe(String f) {
synchronized(f) { System.out.println(f); }}Copy the code
View bytecode instructions after compilation:
0: aload_1
1: dup
2: astore_2
3: monitorenter // Start synchronization with the top element as the lock
4: getstatic #2
7: aload_1
8: invokevirtual #3
11: aload_2
12: monitorexit // Exit synchronization
13: goto 21
16: astore_3
17: aload_2
18: monitorexit
19: aload_3
20: athrow
21: return
Copy the code
reference
[1] : Understanding the Java Virtual Machine in Depth: Advanced JVM Features and Best Practices (3rd edition)
[2] : Java bytecode instructions, make me crazy crazy crazy!
[3] : Java Virtual Machine Specification (Java_SE_7)