This is the 8th day of my participation in the Gwen Challenge in November. See details: The Last Gwen Challenge in 2021.

Detailed introduction to the Java JVM runtime stack structure, and a detailed explanation of method calls, including parse calls and dispatch calls.

JVM for method execution is based on the stack, method call — on the stack, method call completion — off the stack, understanding the JVM runtime stack structure, help us more in-depth analysis, understanding the bytecode and method call execution process.

The study of method calls can help us understand the rules of method overloading and call rewriting from the bytecode level.

1 Runtime stack structure

1.1 the stack frame

** A Stack Frame is a data structure used to support virtual machine method invocation and method execution. It is the Stack element of the VIRTUAL machine Stack in the data area when the virtual machine runs. The ** stack frame stores information about a method’s local variator, operand stack, dynamic linkage, and method return address. Each method from the call to the completion of the process, corresponding to a stack frame in the virtual machine stack from the process of loading and unloading.

When compiling program Code, the maximum local variable table and the deepest operand stack required by a stack frame are fully determined and written into the Code property of the method table, so that the amount of memory allocated for a stack frame is not affected by the runtime data of the program.

The chain of method calls in a thread can be long, with many methods running at the same time. But for the execution engine, only the stack frame at the top of the stack is valid, called the current stack frame, the method associated with this stack frame is called the current method, and the class that defines this method is called the current class. Operations on the local variator and operand stack usually refer to operations on the local variator and operand stack on the current stack frame. If the current method calls another method, or if the current method ends, the method’s stack frame is no longer the current stack frame. When a new method is called, a new stack frame is created and becomes the new current stack frame as control of the program is transferred to the new method. When the method returns, the current stack frame returns the execution result of the method to the previous stack frame. After the method returns, the current stack frame is discarded and the previous stack frame becomes the current frame again.

Stack frames are thread private data and it is not possible to reference another thread’s stack frame in one stack frame.

A typical VM stack frame structure is shown in the following figure:

1.2 Local variation scale

A local variable table is a storage space for a set of variable values used to store method parameters and local variables defined within a method. Variables in the local variable table are only valid for the current method call. When the method call ends, the local variable table is destroyed along with the method stack frame.

When the class class is compiled, the maximum size of a LocalVariableTable for a method is determined in the max_locals data item of the method’s Code property, and local variables are stored in the LocalVariableTable property of the Code property.

The capacity of a local Variable table is in the smallest unit of Variable Slot. The VM specification does not specify the memory space occupied by a Slot. Each Slot should be able to store a Boolean, byte, CHAR, short, int, float, Reference, or returnAddress. In the Java virtual machine, there are only two types of 64-bit data types: long and double. There are two points to note about the data in these local variable tables:

  1. The vm specification does not specify the length of the reference data type, nor does it specify the data structure, but a VM can use reference data to do two things: 1. Through this reference reference, you can directly or indirectly find the actual address index of the object on the Java heap. 2. Through the reference reference, you can directly or indirectly find the type information stored in the method area of the data type to which the object belongs.
  2. For 64-bit long and double data, the virtual machine allocates two contiguous Slot Spaces in high-aligned fashion.

When a method is executed, the virtual machine passes the list of parameter variables using the local variable table. If the method is an instance method, the Slot of every 0 bit index in the local variable table defaults to the reference to the object instance of the method. The hidden local variable can be accessed using the keyword “this” in the method. The rest of the parameters are sorted according to the order in the parameter list, occupying Slot positions starting from 1. After the parameter table is allocated, the remaining slots are allocated according to the order and scope of variables defined in the method body.

It is important to note the local variable does not exist, such as class variables “preparation” phase, class variables will be at the time of class loading after “preparation” and “initialization” stage, even if the programmer not for class variables in the “initialization” gives the initial value, system will still be in the “preparation” phase to the type of the default values, but local variables do not, Local variables do not have a “prepare” stage, so the programmer must manually assign initial values to local variables.

1.2.1 Influence of local variable scale on method invocation

Since the local variable table is in the stack frame, it will occupy the stack space memory. Therefore, if the method has more parameters and local variables, the local variable will swell, so that each method number call will occupy more stack space, and eventually result in the number of nested calls of method number (such as recursion).

public class TestStackDeep {
    private static int count = 0;

    /** * The method has more local variables inside and the maximum number of recursive calls to the method will be smaller *@param a
     * @param b
     * @param c
     */
    public static void recursion(long a, long b, long c) {
        long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10;
        count++;
        recursion(a, b, c);
    }

    /** * The method has fewer local variables inside, and the maximum number of recursive calls to the method will be greater */
    public static void recursion(a) {
        count++;
        recursion();
    }

    public static void main(String[] args) {
        try {
            //recursion(); // Toggle annotating both methods separately, run, and observe the value of count
           recursion(0.0.0);
        } finally{ System.out.println(count); }}}Copy the code

After running it, you can see that methods with fewer local variables can have deeper recursive calls.

1.2.2 Reuse of Solt for local variable scale

Each has its own local variables scope range of bytecode (action), in order to save the stack frame space as far as possible, local variables variables in the Slot where is reusable, method variables defined in the body, the scope will not necessarily method to cover the entire body, if the value of the counter current bytecode PC have is beyond the scope of a variable, The Slot corresponding to this variable can then be used by other variables.

public class SoltReuse {
    public static void solt1(a) {
        // this method is used for both a and b variables
        int a = 1;
        System.out.println(a);
        int b = 1;
    }

    public static void solt2(a) {
        // the a variable is scoped within the code block of the method
        {
            int a = 1;
            System.out.println(a);
        }
        // the b variable is after the scope of the A variable
        int b = 1;
    }

    public static void main(String[] args) {}}Copy the code

Open the class file using jclasslib and find the local variable table for two methods:

It can be seen that the local variable table Solt of solT2 method realizes reuse.

Variables in the local variable table are also judged by the garbage collector as the root node. Objects referenced directly or indirectly by the local variable table are not collected. In some cases, the reuse of slots directly affects the garbage collection behavior of the system.

In the following example, the VM parameter is set to -xx :+PrintGC and the following methods are run respectively.

public class SoltGC {

    public void SoltGC0(a) {
        System.gc();
    }

    public void SoltGC1(a) {
        byte[] b = new byte[5 * 1024 * 1024];
        System.gc();
    }

    public void SoltGC2(a) {
        byte[] b = new byte[5 * 1024 * 1024];
        b = null;
        System.gc();
    }

    public void SoltGC3(a) {{byte[] b = new byte[5 * 1024 * 1024];
        }
        System.gc();
    }

    public void SoltGC4(a) {{byte[] b = new byte[5 * 1024 * 1024];
        }
        int c = 10;
        System.gc();
    }

    public void SoltGC5(a) {
        SoltGC1();
        System.gc();
    }

    public static void main(String[] args) {
        newSoltGC().SoltGC5(); }}Copy the code

Solt0 () is used as a control.

Run solt0(), my GC information is:

[GC (system.gc ()) 5202K->848K(249344K), 0.0011430 secs] [Full GC (system.gc ()) 848K->651K(249344K), 0.0046617 secs]

In the empty method, the Young GC recycles about 5000K as a control. The following example needs to exclude 5000K

Run solt1(), my GC information is:

[GC (system.gc ()) 6000K-> 6000K(249344K), 0.0029231 secs] [Full GC (system.gc ()) 6000K->5776K(249344K), 0.0044659 secs]

The byte array is referenced by the local variable B, so no memory is reclaimed.

Run solt2(), my GC information is:

[GC (system.gc ()) 10322K->912K(249344K), 0.0011081 secs] [Full GC (system.gc ()) 912K->680K(249344K), 0.0048601 secs]

Before garbage collection, set the variable B to null so that byte has no reference.

As you can see, there are about 1000K left after Young GC, and the byte array was reclaimed during Young GC.

Run solt3(), my GC information is:

[GC (system.gc ()) 600022K ->6000K(249344K), 0.0036167 secs] [Full GC (system.gc ()) 6000K->5800K(249344K), 0.0049001 secs]

Garbage collection is performed after the scope of variable B. Since the scope of variable B has expired, GC should reclaim the memory of array B, but the memory of byte array is not reclaimed. Why is this?

Although the code has left the scope of variable B, there is no read or write operation on the fart variable table after that — the Slot occupied by variable B has not been reused by other variables, so the local variable table that is part of the Gc root node remains associated with it. This association is not broken in time. So memory is not reclaimed.

When solt4() is run, my GC information is:

[GC (system.gc ()) 10322K->848K(249344K), 0.0014418secs] [Full GC (system.gc ()) 848K->656K(249344K), 0.0048550 secs]

You can see that the memory is reclaimed because the garbage collection is outside the scope of variable B, and a new variable C is declared. At this point, variable C will reuse the slot of variable B, and references to the array will be cleared, so subsequent GC can reclaim the memory of the array.

Run solt5(), my GC information is:

[GC (system.gc ()) 10322K->6000K(249344K), 0.0030734 secs] 0.0046043 secs [GC (system.gc ()) 5800K->5800K(249344K), 0.0006343 secs] [Full GC (system.gc ()) 5800K->680K(249344K), 0.0041057 secs]

When SoltGC1() returns, its stack frame is destroyed, and the local variables of the natural local variable table are no longer present. Therefore, the memory of the array can be reclaimed on the second GC.

1.3 Operand stack

The operand stack is often referred to as the operation stack, which is also a last-in, first-out stack structure. Many bytecodes need to pass parameters through the operand stack, so it is mainly used to store the intermediate results of the calculation process and serve as temporary storage space for variables during the calculation process.

When a method is first executed, the operand stack is empty. During the execution of the method, various bytecode instructions are written to and extracted from the operand stack. The data types in the operand stack must match the sequence of bytecode instructions, which must be strictly guaranteed at compile time when the program code is compiled, and verified during the data flow analysis during the class validation phase.

For example, iADD, the bytecode instruction for integer addition, needs to ensure that the top two elements of the operand stack are stored as values of type int. Iadd takes the top two elements of the stack, adds them together, and then stores the result in the operand stack.

As with the local variable table, the maximum depth of the operand stack is written at compile time into the max_stacks data item in the Code attribute of the method table. Each element of the operand stack can be any Java data type, including long and double. Long and double occupy two units of stack depth, and other data types occupy several units of stack depth.

The execution engine of a virtual machine is also called “stack-based execution engine”, where the “stack” is the operand stack.

As elements of the virtual machine stack, the two stack frames are theoretically completely independent of each other. However, in most virtual machine implementations, some optimization is done so that the two stack frames overlap. The operand stack of the following stack frame is overlapped with part of the local variable table of the above stack frame, so that part of the data can be shared during method calls without additional parameter replication and transfer. The overlapped process is shown in the figure below:

1.4 Stack frame information

In addition to the local variable table and operand stack, Java stack frames also need some data to support dynamic linking, method return address and other information, collectively referred to as stack frame information.

1.4.1 Dynamic Linking

Each stack frame contains a reference to the method in the runtime constant pool to which the stack frame belongs. This reference is held to support Dynamic Linking during method invocation. The Class file has a large number of symbolic references in the constant pool, and the method invocation instructions in the bytecode take symbolic references to methods in the constant pool as arguments. Some of these symbolic references are converted to direct references during class loading or the first time they are used, which is called static resolution. The other part is converted to a direct reference during each run, and this part is called dynamic linking.

1.4.2 Method return address

Once a method has been executed, there are only two ways to exit the method:

  1. When the execution encounters a return command, the return value is passed to the upper-level Method caller, known as the Normal Method Invocation Completion. Generally, the caller’s PC counter can be used as the return address.
  2. When an exception is encountered and the Method is not processed in the current Method, the Method exits. There is no return value, called the Abnormal Method Invocation Completion, and the return address is determined by the exception table.

When a method exits, it needs to return to where the method was called before the program can continue executing. When a method exits normally, the value of the caller’s PC counter can be used as the return address, which is likely to be stored in the stack frame; When method exception exits, the return address is determined by the exception table. Generally, this part of information will not be saved in the stack frame. After returning to the calling method, the same exception will be thrown in the calling method, and an attempt will be made to find the calling method’s exception table to solve the exception.

The process of exiting a method is essentially the same as removing the current frame from the stack, so the following actions may be performed when exiting:

  1. Restores the local variable table and operand stack of the upper method.
  2. The operand stack that pushes the return value into the caller’s stack frame.
  3. Adjust the value of the PC counter to point to an instruction following the method call instruction.

2 Method Call

A method call is the process of confirming which method to call, not the process of executing the method. Since symbolic references to methods (symbols in the constant pool) are stored in the Class file after Java code is compiled, not direct references to methods (entry addresses of methods in the memory layout), direct references to target methods need to be confirmed at load or run time.

2.1 Parsing Calls

During the parsing phase of class loading, a portion of method symbolic references are converted to direct references. This resolution works only if the method has a determinable invocation version before the program runs, and the invocation version of the method is immutable during runtime. In other words, the call target is determined when the program is written and the compiler compiles. This type of method call is called parsing.

In The Java language, methods that conform to “know at compile time, immutable at run time” mainly include static methods and private methods. The former is directly associated with the type, while the latter is not visible externally. The two methods, by their own nature, cannot override other versions by inheritance or otherwise, and are therefore suitable for parsing during class loading.

The Java Virtual Machine specification provides five method invocation bytecode instructions:

  1. Invokestatic: Invokes static methods.
  2. Invokespecial: Invoke instance constructor methods, private methods, and superclass methods.
  3. Invokevirturl: Invoke (instance) virtual methods.
  4. Invokeinterface: Invokes the interface method, which determines an object implementing the interface at run time.
  5. Invokedynamic: The method referenced by the call point qualifier is dynamically resolved at run time and then executed. The dispatch logic is hardwired into the Java VIRTUAL machine for the previous four invocation instructions. The dispatch logic for InvokeDynamic is determined by the user-specified bootstrap method.

Any method invoked by invokestatic or Invokespecial commands can have a unique invocation version at the parsing stage. Static methods, private methods, instance constructor methods, and parent methods can meet this condition. They convert symbolic references to direct references when the class is loaded. This class of methods is known as non-virtual methods, and the opposite method is virtual (except for final methods).

In addition to the invokestatic and Invokespecial calls, there is another method that is decorated with final. Although final methods are called using Invokevirtual, because it cannot be overridden and no other version is available, there is no need to make polymorphic selection for method recipients. The Java Virtual Machine specification explicitly states that final methods are non-virtual.

The parse call is a static process that is fully determined at compile time, and all symbolic references involved are converted into determinable direct references during the parse phase of the class load without being deferred until runtime. Dispatch calls can be static or dynamic, and can be divided into single Dispatch and multiple Dispatch according to the number of units dispatched. The pair-wise combination of these two types of dispatch forms four dispatch combinations: static single dispatch, static multiple dispatch, dynamic single dispatch and dynamic multiple dispatch.

2.1 Dispatching calls

Java is an object-oriented programming language, because Java has the basic characteristics of object-oriented: inheritance, encapsulation, polymorphism. Dispatching calls will reveal some of the most basic manifestations of polymorphism, such as how “overloading” and “rewriting” are implemented in the Java virtual machine.

2.1.2 Static Dispatch

Case study:

public class StaticDispatch {
    /* Several overloaded methods */
    public void sayHello(Human guy) {
        System.out.println("hello guy");
    }
    public void sayHello(Man guy) {
        System.out.println("hello gentleman");
    }
    public void sayHello(Woman guy) {
        System.out.println("hello lady");
    }
    
    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        //hello guy
        sr.sayHello(man);
        //hello guy
        sr.sayHello(woman);
    }

    static abstract class Human {}static class Man extends Human {}static class Woman extends Human {}}Copy the code

The running results of the program are as follows:

hello, guy hello, guy

Why would an overloaded method of type Human be selected? Before we tackle this problem, let’s look at two important concepts.

Human man = new Man();

For the above code, Human is the static type of the variable and Man is the actual type of the variable. The static type of a variable is determined at compile time, while the actual type is determined at run time. The virtual machine (or, more accurately, the compiler) is judged by the static type of the parameter when reloading, not by the actual type. Because static typing is known at compile time and the Javac compiler decides which overloaded version to use based on the static type of the argument, sayHello(Human) is selected as the call target and a symbolic reference to this method is written into the invokevirtual argument.

You can verify this by looking at the bytecode file using javap -v Staticdispatch.class:

All dispatch actions that rely on static types to locate the version of a method execution are called static dispatch, and a typical application of static dispatch is method overloading. Static dispatch occurs at compile time, so the action to determine static dispatch is not actually performed by the virtual machine.

In addition, while the compiler can determine the overloaded version of a method, in many cases that overloaded version is not “unique” and only a “more appropriate” version can be determined. The main reason for this fuzzy conclusion, which is a “rarity” in the computer world of 0s and 1s, is that literals do not need to be defined, so literals have no explicit static typing, which can only be understood and inferred by linguistic rules.

The following code demonstrates what a “more appropriate” version is:

/** * overloaded methods match priority **@author lx
 */
public class Overload {

    public static void sayHello(Object arg) {
        System.out.println("hello Object");
    }

    public static void sayHello(int arg) {
        System.out.println("hello int");
    }

    public static void sayHello(long arg) {
        System.out.println("hello long");
    }

    public static void sayHello(Character arg) {
        System.out.println("hello Character");
    }

    public static void sayHello(char arg) {
        System.out.println("hello char");
    }

    public static void sayHello(char. arg) {
        System.out.println("hello char...");
    }

    public static void sayHello(long. arg) {
        System.out.println("hello Character...");
    }

    public static void sayHello(Serializable arg) {
        System.out.println("hello Serializable");
    }

    public static void main(String[] args) {
        sayHello('a'); }}Copy the code

Run the code directly and it will print hello char

Since ‘a’ is a char data, it is natural to look for overloaded methods whose arguments are of type CHAR.

If you comment out the sayHello(char arg) method, the output becomes: Hello int

An automatic type conversion occurs. ‘a’ can also represent the number 97 in addition to a string (the Unicode value for the character ‘a’ is the decimal number 97), so overloading of the argument type int is also appropriate.

Continue to comment out the sayHello(int arg) method, and the output will become: Hello long

Two automatic type conversions occur. After ‘a’ is cast to the integer 97, it is further cast to the long integer 97L, matching the overload of the argument type long. I didn’t write overloads for other types like float, double, etc., but in fact the automatic transition can continue multiple times, matching the char->int->long->float->double transitions. Overloads of byte and short will not be matched, because char to byte or short transitions are unsafe.

Continue to comment out the sayHello(long arg) method and the output will change to: Hello Character

An automatic boxing occurs, and ‘a’ is wrapped with its wrapper type java.lang.Character, so an overload of parameter type Character is matched.

Continue commenting out the sayHello(Character arg) method, and the output will change to hello Serializable

“Hello Serializable” is available because java.lang.Serializable is an interface of the java.lang.Character class. So then there’s another automatic transition. Char can be converted to int, but Character can never be converted to Integer; it can only safely be converted to the interface or parent class it implements. Character also implements another interface, java.lang.Comparable, which allows overloaded methods with Serializable and Comparable parameters to have the same priority. The compiler is unable to determine which type to convert to automatically, prompting type ambiguity and refusing compilation. A program must display a static type of a directive literal when called, such as sayHello((Comparable)”a), to compile.

If you comment out the sayHello(Serializable ARg) method, the output becomes hello Object

If there are more than one parent class, the inheritance relationship will be searched from the bottom up, the closer to the top, the lower the priority. This rule applies even if a method call passes in a null parameter value.

Comment out sayHello(Object arg) and the output will be: Hello char…

The seven overloaded methods have been commented down to one, so you can see that the variable-length argument has the lowest overloaded priority, when the character ‘a’ is treated as an array element.

Static methods are resolved at class load time, and static methods can obviously have overloaded versions, and the selection of overloaded versions is done through static dispatch.

Note: Workers developing with IDEA who do not know which version of the method they are using can place the mouse over the calling method, hold Down CTRL, then left-click the method, and it will automatically jump to the calling method.

2.1.3 Dynamic Dispatch

Another important manifestation of dynamic dispatch and polymorphism: overriding Override is closely related. Let’s look at an example of dynamic dispatch:

/** * dynamic dispatch */
public class DynamicDispatch {
    static abstract class Human {
        /** * parent method */
        protected abstract void sayHello(a);
    }

    static class Man extends Human {

        /** * override method */
        @Override
        protected void sayHello(a) {
            System.out.println("man say hello"); }}static class Woman extends Human {
        /** * override method */
        @Override
        protected void sayHello(a) {
            System.out.println("woman say hello"); }}public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        //man say hello
        man.sayHello();
        //woman say hello
        woman.sayHello();
        // Changing the actual type does not change the static type
        man = new Woman();
        //woman say helloman.sayHello(); }}Copy the code

Execute the program, and the output looks like this:

hello man hello woman hello woman

How does the virtual machine call which method? This is obviously not the case because static types are the same two Human variables man and woman that perform different actions when calling sayHello() and man that perform different methods in the two calls. The obvious reason for this is that the actual types of these two variables are different. How does the Java virtual machine dispatch method execution versions based on the actual types? Use the javap -v dynamicdispatch.class command to print the bytecode of this code and try to find the answer:

DynamicDispatch$Human. SayHello is invokevirtual. Aload_1 and ALOad_2 are used to copy the related object from the local variable table to the top of the operation stack. The runtime resolution process of invokevirtual instructions is roughly divided into the following steps:

  1. Find the actual type of the object pointed to by the first element at the top of the operand stack, call it C;
  2. If a method is found in type C that matches both the descriptor and the simple name in the constant, access verification is performed. If it passes, a direct reference to the method is returned, and the search process ends. If not through, it returns the Java. Lang. IllegalAccessError exception;
  3. Otherwise, search and verify step 2 for each parent class of C from bottom to top according to inheritance relationship.
  4. If didn’t find the right way, it throws the Java. Lang. AbstractMethodError anomalies.

Since the first step of the Invokevirtual directive is to determine the actual type of recipient at run time, the Invokevirtual directive in both calls resolves the same class symbol reference to different direct references, a process that is the essence of method rewriting in the Java language. We call this process of determining the version of method execution at run time based on the actual type dynamic dispatch.

2.1.4 Single dispatch versus multiple Dispatch

The receiver of a method and its parameters are collectively referred to as the method’s case, and dispatches can be divided into single dispatches and multiple dispatches depending on how many cases the dispatches are based on. Single dispatch selects the target method based on one case, while multiple dispatch selects the target method based on one case.

Static dispatch in the Java language is a multi-dispatch type because it takes into account both the actual type and method parameters. When executing the Invokevirtual directive, the only thing that affects the virtual machine selection is the actual type, so dynamic dispatch in the Java language is the single-dispatch type.

2.1.5 Dynamic VM assignment

Because dynamic dispatch is a very frequent action, and dynamic dispatch needs to search for the appropriate target method in the method metadata during the method version selection process, virtual machine implementations usually do not directly conduct such frequent searches, but adopt optimization methods for performance reasons.

One of the “stable optimization” methods is to create a Virtual Method Table (vtable, also known as Interface Method Table, also known as itable) in the Method area of a class. Use virtual method table indexes instead of metadata lookups to improve performance. The principle is similar to the virtual function table in C++.

The virtual method table stores the actual entry address of each method. If a method is not overridden in a subclass, the address entry in the subclass’s virtual method table points to the implementation entry of the parent class. The virtual method table is typically initialized during the join phase of the class load.

Related articles:

  1. Java Virtual Machine Specification edition
  2. In-depth Understanding of the Java Virtual Machine
  3. Real Java Virtual Machine

If you don’t understand or need to communicate, you can leave a message. In addition, I hope to like, favorites, pay attention to, I will continue to update Java tutorials!