Talking about JVM (6): method calling process

Previous:
Talking about the JVM (1): Class file analysis
Talking about the JVM (2): Talking about the class loading mechanism (3) : Talking about the
class loader and parent delegation JVM (5): virtual machine stack frame structure

6. Method call process

Java is an object-oriented language with the characteristics of encapsulation, inheritance, and polymorphism. Through inheritance or other means, a method can be overridden into multiple versions. Before the method code is executed, the version of the called method is confirmed. This process is called Method Invocation. Method calls can be understood from two aspects: Resolution and Dispatch.

6.1. Analysis

​ The target method of the method call is a symbolic reference in the constant pool in the Class file (see JVM (1): Class file parsing for details ), and some symbolic references will be converted into direct references during the parsing phase of class loading. The version of the method to be called has been determined at the time, that is, "knowable at compile time, immutable at runtime", and the call of this type of method is resolution (Resolution).

​ Typical methods that can be invoked through parsing include static methods, private methods, constructors, parent class methods, and final modified methods. Because static methods are class-related and will not be rewritten; private methods cannot be accessed externally and will not be rewritten; construction methods will not be rewritten; parent class methods refer to methods that subclasses can inherit and rewrite; Final modified methods cannot be overridden. Therefore, the above five methods can determine the only executed version at compile time, and can resolve symbol references into direct references at class loading time. These methods are collectively referred to as "non-virtual methods" (Non-Virtual Method), on the contrary Other methods are called "virtual methods" (Virtual Method).

There are 5 bytecode instructions in the Java virtual machine for calling different types of methods:

  • invokestatic: used to call static methods;
  • invokespecial: used to call instance constructors, private methods, and methods of parent classes (such as those called by super);
  • invokevirtial: used to call all virtual methods, and final methods (although it is a non-virtual method);
  • invokeinterface: used to call the interface method, and then determine an object that implements the interface at runtime;
  • invokedynamic: The method referenced by the call point qualifier is dynamically resolved at runtime, and then the method is executed.
package com.menglaoshi.test;

/**
 * @author 专治八阿哥的孟老师
 */
public class TestClass02 {
    
    
    public static void staticMethod(){
    
    }

    private void privateMethod(){
    
    }

    public void test(){
    
    
        staticMethod();
        privateMethod();
    }
}

insert image description here

​ The parsing call is completely determined at the compilation stage. In the parsing stage of class loading, all the symbolic references involved will be converted into explicit direct references. It does not need to be delayed until the runtime to complete. It is a static process. Another more complex method call is dispatch (Dispatch).

6.2. Dispatch

The dispatch (Dispatch) call process is closely related to Java's polymorphic features, such as method overloading and method rewriting. According to whether the method call is determined by the compiler or at runtime, dispatch can be divided into static dispatch and dynamic dispatch; according to the caller and the number of parameters, it can be divided into single dispatch and multiple dispatch.

6.2.1. Static dispatch

package com.menglaoshi.test;

/**
 * @author 专治八阿哥的孟老师
 */
public class StaticDispatch{
    
    
    public void sayHello(Human guy){
    
    
        System.out.println("hello,Human");
    }
    public void sayHello(Man guy){
    
    
        System.out.println("hello,Man");
    }

    public static void main(String[] args) {
    
    
        Human man=new Man();
        StaticDispatch dispatch=new StaticDispatch();
        dispatch.sayHello(man);
    }
}

​ The two sayHello() methods in the above code are typical method overloads, and the result of the test code is "hello, Human". We want to analyze the reasons for this result from the perspective of the virtual machine.

Human man = new Man();

In the above code, Human is called the static type (Static Type) or appearance type (Apparent Type) of the variable, and the following Man is the actual type (Actual Type) or runtime type (Runtime Type) of the variable. Both the appearance type and the actual type may change. The change of the appearance type is known at compile time, and the actual type change is determined at runtime.

public static void main(String[] args) {
    
    
    StaticDispatch dispatch = new StaticDispatch();
    //实际类型变化,运行时才指导是Human还是Man
    Human man = (new Random().nextBoolean()) ? new Human() : new Man();
    //外观类型,编译器就知道最终转成Man(运行时可能报错)
    dispatch.sayHello(man);
}

​ The actual type of the variable man is determined based on a randomly generated Boolean value, and its appearance type Human can be known at compile time, and the appearance type can be changed through mandatory type conversion during use, which is still known at compile time (if the actual type is Human , forced conversion to Man will report an error ClassCastException at runtime).

​ When the compiler encounters an overloaded method, it judges based on the appearance type. The dispatch action that depends on the appearance type (static type) to determine the execution version of the method is called static dispatch, and this part can also be classified as analysis.

The method version confirmed by static dispatch is not unique, and the compiler will infer a relatively suitable version.

package com.menglaoshi.test;

/**
 * @author 专治八阿哥的孟老师
 */
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(Serializable arg) {
    
    
        System.out.println("hello Serializable");
    }

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

The above code will output "hello char" after running, there is no doubt about it.

Comment out the sayHello(char arg) method, run to get "hello int", because 'a' can also represent 97 (Unicode).

Comment out sayHello(int arg), the output becomes "hello long", char is converted to int and then further converted to long.

​ Continue to comment out sayHello(long arg) and the result becomes "hello Character", and the 'a' of char type is encapsulated into an object type.

​ Comment out sayHello(Character arg) and the output becomes "hello Serializable", because java.lang.Character implements the java.lang.Serializable interface.

​ If you continue to comment out sayHello(Serializable arg), the output becomes "hello Object".

​ Comment out sayHello(Object arg), and finally output "hello char..."

​ If there are multiple parent classes, the search will start from bottom to top in the inheritance relationship. The closer to the upper layer, the lower the priority, and the parameter can be passed in null.

6.2.2. Dynamic dispatch

The process of selecting a method version by its actual type at runtime is called dynamic dispatch. Dynamic dispatch is closely related to method overriding.

public abstract class Human {
    
    
    abstract void sayHello();

    public static void main(String[] args) {
    
    
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

class Man extends Human {
    
    
    @Override
    void sayHello() {
    
    
        System.out.println("man say hello");
    }
}

class Woman extends Human {
    
    
    @Override
    void sayHello() {
    
    
        System.out.println("woman say hello");
    }
}

The result of running the above code is as follows:

man say hellowo
man say hello
woman say hello

​ This is the running result of a typical method rewriting. The appearance types of the two variables man and woman are both Human, but the actual types are different, resulting in different methods to call. From the perspective of the virtual machine to analyze why there is such a result, the key bytecode is as follows:

insert image description here

​ From the bytecode, the method called by invokevirtual is the #6 constant in the constant pool, and the #6 constant is the sayHello() of the Human class.

insert image description here

​ On the surface, the method of invoking through the two variables of man and woman is the same, but in fact, the invokevirtual command will be divided into multiple steps when parsing:

  1. Note that the actual type referenced by the top of the current stack is C, and the method described in the constant pool is M. If there is an overridden method M in class C , then execute method M;
  2. Otherwise, follow the inheritance relationship from bottom to top until the rewritten method M is found , the access permission verification passes, and the method M is executed;
  3. If no subclass overrides method M, and M is not abstract in the parent class that originally declared M, M is executed.

​ The method of invokevirtual execution in the virtual machine specification is described as "a declaration for an instance method that overrides the resolved method", where the meaning of overrides is:

  1. Note that the parent class is A, the subclass is C, the parent class method Ma, and the subclass method Mc;

  2. C is a subclass of A, Mc and Ma have the same method description, and Mc cannot be private;

  3. Ma is public, protected, or has no access modifier and is in the same package as C; or Mc rewrites a method m', and m' rewrites Ma;

    When invokevirtual is executed, the method version is determined by the actual type, which is the essence of method rewriting.

insert image description here

​ It should be noted that there is only the concept of virtual methods in Java, but no virtual fields. When the subclass declares a field with the same name as the parent class, the field of the subclass will overwrite the field of the parent class. If the access permission is allowed, the field of the parent class can be accessed through super, and the subclass overrides the field of the parent class. Not required to be the same type. There is no polymorphism in the field. Assuming that the field F is accessed through the method of class C, then the F you see is the field that can be seen in class C: if F is not declared in C, F is the parent class; if F is declared in C , then F is in the subclass.

public class Parent {
    
    
    String a;

    public Parent() {
    
    
        a = "P";
        System.out.println("P:" + a);
        test();
    }

    public void test() {
    
    
        System.out.println("Parent:" + a);
    }

    public static void main(String[] args) {
    
    
        new Child();
    }
}

public class Child extends Parent {
    
    
    String a;

    public Child() {
    
    
        a = "C";
        System.out.println("C:" + a);
        test();
    }

    public void test() {
    
    
        System.out.println("Child:" + a);
    }
}

The output of the above code is as follows:

P:P
Child:null
C:C
Child:C

​ First, to construct a subclass object, the parent class must be implicitly initialized first, and the construction method cannot be rewritten. Therefore, when the Parent method is executed, the variable a is assigned to "P". The a accessed through the Parent is a in the Parent class, and the output "P:P"; however, the test() method is rewritten by the subclass, and the action of constructing the object is triggered by the subclass. The test() method is a virtual method call, and the method in the current subclass is called. See The obtained field a should be in the subclass, but at this time the subclass construction method has not been executed, so "Child: null" is output.

6.2.3. Single dispatch and multiple dispatch

​ The receiver of the method and the parameters of the method are collectively referred to as the quantity of the method. The assignment based on one quantity is called single dispatch, and the assignment based on multiple quantities is called multi-dispatch. Demonstrate with the following code:

public class SuperClass {
    
    
    public void test(String arg) {
    
    
        System.out.println("super string");
    }

    public void test(int arg) {
    
    
        System.out.println("super int");
    }
}

public class SubClass extends SuperClass {
    
    
    public void test(String arg) {
    
    
        System.out.println("sub string");
    }

    public void test(int arg) {
    
    
        System.out.println("sub int");
    }

    public static void main(String[] args) {
    
    
        SuperClass c = new SubClass();
        c.test(1);
    }
}

The result of running the above code is "sub int", no doubt. The dispatch process in the compilation phase is static dispatch. At this time, the method version is first determined by the type of the caller, and secondly, which overloaded method to execute is selected according to the parameters. The method version is determined by two parameters, so static dispatch is multiple dispatch. .

​ The dispatch in the running phase is dynamic dispatch, because the compile phase has determined to call test(int), when executing the virtual method, you only need to know whether the actual type of the caller is SuperClass or SubClass, and you don’t need to look at the parameter list of the method, so Only one quantity needs to be known, and dynamic dispatch is single dispatch.

In summary, Java is a static multi-dispatch and dynamic single-dispatch language.

6.2.4. Implementation of virtual machine dynamic allocation

​ The execution of the virtual method needs to look up the metadata of the type according to the inheritance relationship. In order to improve the operation efficiency and avoid repeated searches, the virtual machine has established a virtual method table (Virtual Method Table, vtable for short) in the method area. The interface used The method table is called Interface Method Table (itable for short). Use virtual method table indexes instead of metadata searches to improve performance. The actual entry of each method is stored in the virtual method table. If the method has not been rewritten, the address entry in the subclass virtual method table is the same as that of the parent class, pointing to the entry of the parent class implementation.

insert image description here

​ Methods with the same signature should have the same index number in the virtual method table of the parent class and the subclass, so that when the type is changed, only the virtual method table to be searched needs to be changed, and the virtual method table can be retrieved from different virtual method tables. Convert the desired entry address by index. The virtual method table is generally initialized during the connection phase of class loading. After the initial values ​​of the variables of the class are prepared, the virtual machine also initializes the virtual method table of the class.

Finally, what does the following code output?

public class Father {
    
    
    private int i = method();
    private static int j = staticMethod();

    static {
    
    
        System.out.println(1);
    }

    public Father() {
    
    
        System.out.println(2);
    }

    {
    
    
        System.out.println(3);
    }

    public int method() {
    
    
        System.out.println(4);
        return 1;
    }

    public static int staticMethod() {
    
    
        System.out.println(5);
        return 1;
    }
}
public class Son extends Father {
    
    
    private int i = method();
    private static int j = staticMethod();

    static {
    
    
        System.out.println(6);
    }

    public Son() {
    
    
        System.out.println(7);
    }

    {
    
    
        System.out.println(8);
    }

    public int method() {
    
    
        System.out.println(9);
        return 1;
    }

    public static int staticMethod() {
    
    
        System.out.println(10);
        return 1;
    }

    public static void main(String[] args) {
    
    
        new Son();
        new Son();
    }
}

Guess you like

Origin blog.csdn.net/u011731544/article/details/129925162