preface

In the Learning JVM series, you’ve covered the JVM specification, the Class file format, and how to read bytecode, ASM bytecode processing, the life cycle of a Class, and custom Class loaders. This article introduces the bytecode execution engine, understanding the stack frame structure, local variable storage, operand storage, dynamic dispatch and other contents involved in the actual operation of Java programs

If you’re interested in JVMS, bytecodes, Class file formats, ASM bytecode processing, Class loading and custom Class loaders, and memory allocation, check out the previous article for more

Android engineers learn the basics of JVM(5)- memory allocation

Android engineers learn about JVMS (4)- class loading, connecting, initialization, and unloading

Android engineers learn how to use THE JVM(iii)- bytecode framework ASM

Android Engineer learning JVM(II)- Teaches you to read Java bytecode

Android Engineers learn about JVM(I)- Overview of the JVM

1. Overview of bytecode execution engine

The bytecode execution engine of the JVM basically parses and processes the bytecode input, and finally outputs the execution result

There are currently two implementations, either directly interpreting bytecode execution through an interpreter, or producing native code through an just-in-time compiler, which is compilation execution. This part involves the implementation of virtual machines. Interpretation execution may be used for less times of execution, while compilation execution may be used for more times of execution, which can optimize performance to a certain extent.

2, the stack frame

In the last article we introduced for the first time that each method in the Java virtual machine stack is a stack frame. This summary will cover stack frames in detail.

2.1 Overview of stack frames

Stack frames are data structures that support method invocation and method execution by the JVM. Stack frames are created with method invocation and destroyed with method completion. The stack frame stores the local variables, operand stack, dynamic link, method return address and other information of the method

Stack frame and frame stack model are shown as follows:

In the last article, we talked about how stacks are thread private, so you can understand that better here. When a method is called, the stack frame associated with the method is pushed onto the stack, and when the call is complete, the stack is removed. A stack frame is actually equivalent to running the program, each thread runs its own logic, so it is easy to understand that each thread has its own stack frame. Let’s focus on the details of stack frames. I think you’ll get a sense of how the program works, right

2.2. Local variation scale

Local variable table: Storage space for method parameters and local variables defined within a method

1. Local variable tables are stored in variable slots. Currently, one slot stores data types with less than 32 bits

2. For instance methods, slot 0 holds this, which is then assigned to the argument list from 1 to n

3. Slot allocation is based on the order and scope of variables defined inside the method body

The following is a look at the actual case:

public class Test {

    public int add(int a, int b) {
        int c = a + b;
        returna+b+c; }}Copy the code

After compiling the class bytecode, use the javap -v test. class command to view the add method bytecode as follows:

From the figure, the final LocalVariableTable is the LocalVariableTable we introduced above. As you can see, there are currently four slots. The 0th slot contains this, and the first and second slots are method parameters A and b. The third slot stores the local variable C in the method, which is arranged sequentially.

Look again at the Code section of assembly Code, which is the bytecode representation of the Code we wrote.

0: iloAD_1 ------------------ load first1Local variables1: iload_2------------------ load first2Local variables2: iadd------------------ adds the first local variable to the second3: istore_3------------------ Stores the result of the addition to the third local variable// c=a+b
Copy the code

In other words, the local variable table is used to store method parameters and local variables within a method. But in a+ B + C, we look at the actual execution of the assembly, loading A, B performing a+ B, and then loading C, adding again. So how does it work that the outcome of a plus b is not our local variable? This is where the operand stack comes in, which is covered in the next section.

Another feature of the slot section that needs to be highlighted is reuse, which sometimes leads to some strange phenomena.

    public static void main(String[] args) {{byte[] bs = new byte[2*1024*1024];
        }
        // This line of code will affect whether bs memory can be freed
        //int a = 1;
        System.gc();
        System.out.println("totalMemory===" + Runtime.getRuntime().totalMemory()/1024.0/1024.0);
        System.out.println("freeMemory===" + Runtime.getRuntime().freeMemory()/1024.0/1024.0);
        System.out.println("maxMemory===" + Runtime.getRuntime().maxMemory()/1024.0/1024.0);
    }
Copy the code

Before running this program, configure to print GC logs as follows:

When a=1 is commented out for many times, the memory result is similar. Select one of them and the situation is as follows:

TotalMemory = = = 123.0

FreeMemory = = = 120.07547760009766

MaxMemory = = = 1820.5

The program takes up about 3M space, and THE BS memory has not been released. The memory here depends on my machine, so just focus on the big picture

Comment out the line int a = 1. Running results:

TotalMemory = = = 123.0

FreeMemory = = = 122.07544708251953

MaxMemory = = = 1820.5

The 2M occupied by BS was recovered by GC, and the test results were consistent for many times. Why ???? Why does writing a=1 affect GC?

In fact, slot is multiplexed, and its slot can be multiplexed when it is out of scope of BS.

If a=1 is commented out, it still refers to the memory space of 2M, so the memory space of 2M still has a reference and cannot be reclaimed.

When the comment a=1 is removed, A has reused the slot of BS, so the space 2M is not referenced and can be recycled. The memory is freed up.

Isn’t that amazing? So it makes a lot of sense to say that when big data is no longer used, we should set the bit to NULL. In this case, writing bs=null is the best practice.

2.3 operand stack

Operand stack: Used to store the data operated by each instruction during the execution of a method

Again, use this example:

0: iload_1 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- loading a local variable 1:1 iload_2 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- loading a local variable 2 2: Iadd -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- will be the first local variables and the second local variables add 3: istore_3 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- will be added to store the results of the third local variablesCopy the code

When we load the first local variable, what we’re really saying is we’re putting the first local variable on the operand stack. Translate this code again, from the operand stack perspective

0: iloAD_1 ------------------ will be the local variable1The pressure into the stack1: iload_2------------------ will be the local variable2The pressure into the stack2: iadd------------------ pops two numbers into the stack and pushes the result onto the stack3: istore_3------------------ Pushes the top value off the stack and assigns it to the first value3Local variables4: iloAD_1 ------------------ will be the local variable1The pressure into the stack5: iload_2------------------ will be the local variable2The pressure into the stack6: iadd------------------ pops two numbers into the stack and pushes the result onto the stack7: iload_3------------------ will be a local variable3The pressure into the stack8: iadd------------------ pops two numbers into the stack and pushes the result onto the stack9: ireturn------------------ Pushes the top value off the stack and returns the resultCopy the code

2.4. Dynamic linking

Each stack frame holds a reference to the method that the stack frame belongs to in the runtime constant pool to support dynamic linking of method invocation procedures

So how does the virtual machine find methods at runtime? We often override methods, override methods in programs.

Method invocation: Method invocation determines which method to call and does not involve the execution flow within the method

There are two general cases:

1, part of the method is directly in the class load parsing phase, then determine the direct reference relationship. Static methods, private methods, superclass methods

2. For instance methods, also known as virtual methods, because of overloading and polymorphism, dynamic delegation is required at runtime

There are two positioning methods corresponding to these two cases:

Static dispatch: Dispatch methods that rely on static types to locate the version of a method execution, such as method overloading

2. Dynamic dispatch: The dispatch method of method execution versions, such as method rewriting, is located based on the actual type of the runtime

2.5. The method returns the address

Refers to the address returned by the method after execution.

public static void main(String[] args) {
    Test test = new Test();
    // The test.add method must return to the main method to proceed with system.out. println to print the result
    int result = test.add(1.2);
    System.out.println("result == " + result);
}
Copy the code

3, summary

The most important part of the bytecode execution engine is understanding stack frames. The key to understanding of this article is:

1. The relationship between frame stack and stack frame

2. What content does the stack frame data structure contain

3. The difference between the function of local variable table and the function of operand stack

4, virtual machine method call, this section mainly learn several concepts, such as static dispatch, dynamic dispatch and so on