The method call

Method invocation is not the same as the code in the method being executed. The only task of the method invocation phase is to determine the version of the method being called (i.e. which method to call).

parsing

The target method of all method calls is a symbolic reference in the constant pool in the class file, and some of these symbolic references are converted to direct references during the parsing phase of the class load. This resolution is possible only if the method has a determinable invocation version before the program actually runs. And the called version of this method is immutable at run time. In other words, the call target is determined as soon as the program code is written and the compiler compiles. The invocation of this type of method is called Resolution.

Different types of methods are called, and different instructions are designed in the bytecode instruction set

Compile time is known, but run time is immutable

In The Java language, methods that meet the requirement of compile time and runtime immutable are mainly divided into static methods and private methods. The former is directly associated with the type, while the latter cannot be accessed externally. The respective characteristics of these two methods determine that they cannot be overwritten by inheritance or other means. They are therefore suitable for parsing during class loading.

Virtual methods vs. non-virtual methods

As long as the method can be invoked by invokestatic and Invokespecial instructions, the unique invocation version can be determined in the parsing stage. In Java language, methods that meet this condition include static method, private method, instance constructor and parent method. Together with the final modified method (although it is invoked using the Invokevirtual directive), these five method calls resolve symbolic references to direct references to the method when the class is loaded. These methods are collectively referred to as “non-virtual methods”, whereas other methods are called “Virtual methods”.

Method static parsing demo

public class StaticResolution {

    public static void sayHello(a) {

        System.out.println("hello world");

    }

    public static void main(String[] args) {

        StaticResolution.sayHello();

    }

}

Copy the code

Using the javap command to look at the bytecode corresponding to this program, you can see that the sayHello() method is indeed called by invokestatic, and that the version of the method is explicitly fixed in the bytecode directive’s arguments at compile time as a constant pool entry (#5 is the constant pool index) :

5 represents the index in the constant pool from which symbolic references to method calls are known

The dispatch

As we all know, Java is an object-oriented programming language, because Java has three basic characteristics of object-oriented: inheritance, encapsulation and polymorphism.

The dispatch invocation process will reveal some of the most basic manifestations of polymorphism, such as how “overloading” and “rewriting” are implemented in the Java virtual machine.

Static dispatch and overloading

public class StaticDispatch {

  static class Human {



  }

  static class Man extends Human{



  }

  static class Woman extends Human{



  }

  public void sayHello(Human human) {

    System.out.println("hi, guy");

  }

  public void sayHello(Man man) {

    System.out.println("hi, man");

  }

  public void sayHello(Woman woman) {

    System.out.println("hi, woman");

  }

  public static void main(String[] args) {

    Human man = new Man();

    Human woman = new Woman();

    StaticDispatch sd = new StaticDispatch();



    sd.sayHello(man); // hi, guy

    sd.sayHello(woman);// hi, guy

  }

}

Copy the code

The result shows that the virtual machine selected the overloaded version with the parameter type Human. Why? Before solving this problem, let’s define two key concepts with the following code:

Human man = new Man();

Copy the code

We call the “Human” in the code above “Static Type”, or “appearance Type”. The following “Man” is called the “Actual Type” or “Runtime Type” of the variable.

The difference is that the static type changes only at use, the static type of the variable itself is not changed, and the final static type is known at compile time. The result of the actual type change is determined at run time.

This can be illustrated with a practical example, such as the following code:

// The actual type changes

Human human = (new Random()).nextBoolean() ? new Man() : new Woman();



// Static type change

sr.sayHello((Man) human)

sr.sayHello((Woman) human)

Copy the code

The actual type of the object human is mutable, and at compile time it is completely “Schrodinger’s Man”. Whether it is Man or Woman must be determined at this point in the program. The static type of human is human, and it can be changed temporarily when used (as in the forced transformation of sayHello() method), but the change is still known at compile time. Two calls to sayHello() make it clear at compile time whether the change is Man or Woman.

Now that we’ve explained the concept of static versus real typing, let’s look at the example code above:

For the two calls to the sayHello() method in main(), which overloaded version is used depends entirely on the number of arguments and data types passed in, given that the method receiver has already identified the object SR. The code intentionally defines two variables of the same static type but different actual types, but the virtual machine (or, more accurately, the compiler) uses the static type of the parameter when reloading, not the actual type. Since static types are known at compile time, at compile time the Javac compiler determines which overloaded version to use based on the static type of the argument, so ayHello(Human) is selected as the call target. Write symbolic references to this method to the arguments of the two Invokevirtual directives in the main() method

All dispatch actions that rely on static types to determine the version of a method’s execution are called static dispatch. The most 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, which is why some sources choose to classify it as “parsing” rather than “dispatching.”

Dynamic dispatch and rewrite

With static dispatch in mind, let’s look at the implementation of dynamic dispatch in the Java language, which is closely related to Override, another important manifestation of polymorphism in the Java language

public class DynamicDispatch {

  static abstract class Human {

    protected abstract void sayHello(a);

  }



  static class Man extends Human {

    @Override

    protected void sayHello(a) {

      System.out.println("man, say hello");

    }

  }



  static class Woman extends Human {

    @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.sayHello(); // man, say hello

    woman.sayHello();// woman, say hello

  }

}

Copy the code

We print the bytecode of this code using the javap command:

Lines 0 to 15 of the bytecode are preparatory work: Call the instance constructor of type man and woman, and store references to these two instances in slot 1 and 2 of the local variable table (astore_1, astore_2). These actions correspond to these two lines in the Java source code:

Human man = new Man();

Human woman = new Woman();

Copy the code

The next lines 16 to 21 are critical. The ALOad_1 and ALOad_2 instructions on lines 16 and 20 push references to the two objects just created, the owners of the sayHello() method to be executed, called receivers, to the top of the operand stack, respectively. Lines 17 and 21 are the method call instructions. From a bytecode perspective, both the instruction (both invokevirtual) and the parameter (both constants of item 22 in the constant pool, which the comment indicates is a symbolic reference to human.sayHello ()) are exactly the same. But the two instructions ultimately execute in different ways.

It seems that the key to solving the problem is to start with the Invokevirtual directive itself, and figure out how it determines the version of the invoked method and how it implements polymorphic lookup.

According to the Java Virtual Machine Specification, the runtime resolution process of the Invokevirtual directive is roughly divided into the following steps:


It is because the
invokevirtualThe first step in instruction execution is determination at run time
The receiverThe actual type, so in both calls
invokevirtualInstead of resolving symbolic references to methods in the constant pool to direct references, directives choose method versions based on the actual type of method recipients, a process that is the essence of method rewriting in the Java language.