虚拟机字节码执行引擎(二)

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一任务就是确定调用哪个方法,暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用时最普遍最频繁的操作,但前面讲过,class文件的编译过程并不包含连接步骤,一切方法调用在class文件里存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得先对复杂,需要在类加载期间,甚至运行期间才能确定目标方法的直接引用。

解析

所有方法调用中的目标方法在class文件里面都是一个常量池中的符号引用,在类加载阶段解析阶段,会将一部分符号引用转化为直接引用,这种解析能成立的前提是,方法在程序真正运行之前就有一个可确定的调用版本(只有一个类可以调用方法),并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法调用称为解析

在Java中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了他们不可能通过继承或别的方式重写其他版本,因此他们都适合在类加载阶段解析。

在Java虚拟机中提供了5条方法调用字节码指令,分别为:

invokestatic:调用静态方法

invokespecial:调用实例构造器init方法、私有方法和父类方法

invokevirtual:调用所有虚方法

invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。

invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器,父类方法4类,他们在类加载时就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final)。下面例子演示了一个最常见的解析调用的例子,此样例中,静态方法sayHello只能属于类型staticResolution没有任何手段可以覆盖或隐藏这个方法。

public class StaticResolution {
   public static void sayHello(){
      System.out.println("hello world");
   }

   public static void main(String[] args) {
      StaticResolution.sayHello();
   }
}

javap -verbose查看这段程序的字节码,发现的确是通过invokestatic命令来调用sayHello方法的。

Java中的非虚方法除了使用invokestatic、invokespecial调用的方法之外还有一种,就是final修饰的方法。虽然final方法时使用invokevirtual指令来调用,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接受者进行多态选择,也可以说多态选择的结果肯定是唯一的。在Java语言规范中明确说明了final方法时一种非虚方法。

解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。而分派调用可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况

分派

Java之所以面向对象,是因为继承、封装、多态。下面讲解的分派调用过程将会揭示多态性特征的一些最基本的体现,如重载和重写在Java虚拟机之中是如何实现的,这里的实现不是语法上该如何写,我们关心的仍然是虚拟机中如何确定正确的目标方法。

1、静态分派

方法静态分派演示:

public class Test {

   static abstract class Human{}

   static class Man extends Human{}

   static class Woman extends Human{}

   public void sayHello(Man guy){
      System.out.println("hello,gentleman!");
   }

   public void sayHello(Woman guy){
      System.out.println("hello lady!");
   }

   public void sayHello(Human guy){
      System.out.println("hello,guy!");
   }

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

为什么会选择执行参数类型为Human的重载呢?

我们把上面代码中的Human称为变量的静态类型(static type),或者叫外观类型(Apparent Type),后面的man则称为变量的实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才能确定,编译器在编译程序时并不知道一个对象的实际类型是什么。例如:

main里面两次sayhello方法调用,在方法接受者已经确定是对象“t”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中刻意地定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确说是编译器)在重载时是通过参数的静态类型而不是实际类型做出判断依据的。并且静态类型编译器可知,因此,在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载的版本,所以选择了sayHello作为调用目标,并把这个方法的符号引用写到main方法的两条invokevirtual指令的参数中。

所有依赖静态类型来定位方法执行版本的分派称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定方法的重载版本,但很多情况下这个重载版本并不是唯一的,往往只能确定一个“更加合适的”版本。产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断。下面代码演示了何为“更加合适的版本”。

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

   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(char arg){
      System.out.println("hello char");
   }

   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(Serializable arg){
      System.out.println("hello Serializable");
   }

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

}

如果注释掉char,int等等方法,他会按照char->int->long->float->double的顺序转型进行匹配。但不会匹配到byte和short类型的重载,因为char到byte或short的转型是不安全的。我们继续注释掉long方法,那输出会变为:

hello character

这时发生了一次自动装箱,‘a’被包装为它的封装类型java.lang.Character,所以匹配到了参数类型为character的重载,继续注释掉character方法,那输出会变为:

hello Serializable

这个输出有点怪,一个字符或数字与序列化有什么关系?出现hello serializble,是因为serializable是character类实现的一个接口,当自动装箱之后发现还是找不到装箱类,但是找打了装箱类实现的接口类型,所以又发生了一次自动转型。char可以转型为int,但是character绝不可以转型为Integer,他只能安全的转型为它实现的接口或父类。如果同时有两个重载的父类,那它们的优先级是一样的。编译器无法确定要自动转型为哪种类型,会提示类型模糊,拒绝编译。程序必须在调用时显式指定字面量的静态类型,如:sayHello(Comparable<Comparable> 'a')才能编译通过。继续注释,就轮到object了,即使传null也可以。最后只剩一个变参了,可见变参的重载优先级是最低的,这时候字符'a'被当做了一个数组元素。

解析与分派这两者之间的关系并不是二选一的排他关系,他们在不同层次上去筛选、确定目标方法的过程。例如,静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。

2.动态分派

了解了静态分派,我们接下来看下动态分派的过程,它和多态性的另外一个重要体现——重写有着很密切的关联。我们还是用前面的Man和Woman的例子来讲解动态分派。

这个运行结果理所当然,现在问题还是和前面一样,虚拟机是如何知道要调用哪个方法的?

显然这里不可能再根据静态类型来决定,因为静态类型同样都是human的两个变量man和woman在sayHello时执行了不同的行为,兵器变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?我们javap -verbose一下:

0-15行的字节码是准备工作,作用是建立man和woman的内存空间,调用man和woman类型的实例构造器,将这两个实例的引用存放在第1、2个局部变量表slot之中,这个动作也就对应了代码中的:

Human man = new Man();

Human woman = new Woman();接下来16-21句是关键部分,16、20两句分别把刚刚创建的两个对象的引用压到栈顶,这两个对象是将要执行的sayHello方法的所有者,称为接受者。17,21句是方法的调用指令,这两条调用指令从字节码角度来看,无论是指令还是参数(都是常量池中第22项常量)都是完全一样的,但是这两句指令最终执行的目标方法并不相同

invokevirtual指令执行的第一步就是在运行期确定接受者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程是Java重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派称为动态分派。

3.单分派与多分派

方法的接受者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派是根据多个宗量对目标方法进行选择。

例子:

public class Dispatch {

   static class QQ{}

   static class _360{}

   public static class Father{
      public void hardChoice(QQ arg){
         System.out.println("father choose qq");
      }
      public void hardChoice(_360 arg){
         System.out.println("father choose 360");
      }
   }

   public static class Son extends Father{

      @Override
      public void hardChoice(QQ arg) {
         System.out.println("son choose qq");
      }

      @Override
      public void hardChoice(_360 arg) {
         System.out.println("son choose 360");
      }

   }

   public static void main(String[] args) {
      Father father = new Father();
      Father son = new Son();
      father.hardChoice(new _360());
      son.hardChoice(new QQ());
   }


}

猜你喜欢

转载自blog.csdn.net/qq_37113604/article/details/90632971