JVM之坑:深入理解Java解析和分派

解析

这是非常容易混淆的点,因为它可以表示:

1,解析阶段

我们知道,类的加载过程包含七个阶段:加载验证准备解析初始化使用卸载,七个阶段顺序开始,交叉进行。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,是这七个阶段之一。

  • 直接引用可以是直接指向目标的指针相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。简单来讲,解析就是从字面符号到内存地址,从内存无关到内存有关的过程。
  • 虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
  • 同一个符号引用进行多次解析请求是很常见的事情:
    • 静态解析这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。直接操作字节流Class文件的常量池 除invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。静态解析具有幂等性,即同一个实体中的同一个符号引用,第一次成功后续皆成功;同样的,第一次失败后续皆异常且相同异常。
    • 动态连接另外一部分符号引用将在每一次运行期间转化为直接引用,这部分称为动态连接。操作的是方法区中的运行时常量池 对于invokedynamic指令,上面规则则不成立。当碰到某个前面已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对于其他invokedynamic指令也同样生效。因为invokedynamic指令的目的本来就是用于动态语言支持,这里“动态”的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。

2,解析调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。其中在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成,换句话说,编译器进行编译时就必须确定下来。这类方法的调用称为解析调用

*Java虚拟机里面提供了5条方法调用字节码指令

指令 方法
invokestatic 调用静态方法。
invokespecial 调用实例构造器<init>方法、私有方法和父类方法。
invokevirtual 调用所有的虚方法。
invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象。
invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
  • 只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final方法,后文会提到)。
  • Java中的非虚方法除了使用invokestatic、invokespecial调用的方法之外还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的。
  • 所以,静态方法、私有方法、实例构造器、父类方法以及final方法的调用都是解析调用

分派

方法调用还具有分派调用这种方式。首先我们要理解什么是静态类型实际类型,例如:

Human man=new Man();
  • 静态类型。“Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type)。静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;
  • 实际类型。后面的“Man”则称为变量的实际类型(Actual Type)。实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。例如下面的代码:
//实际类型变化
Human man=new Man();
man=new Woman();
//静态类型变化
sr.sayHello((Man)man);
sr.sayHello((Woman)man);

了解了静态类型实际类型,我们可以更好地区别静态分派动态分派

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

总述

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

同样的,静态分派调用动态分派调用,只是发生在不同阶段分派调用方式而已。可以通过下面代码理解:

Human man=new Man();
Human woman=new Woman();
man.sayHello();
woman.sayHello();
man=new Woman();
man.sayHello();

输出结果:

man say hello
woman say hello
woman say hello
  • 首先,编译阶段编译器的选择过程,也就是静态分派的过程。最终产物是产生了三条invokevirtual指令,三条指令的参数同为常量池中指向Human.sayHello()方法的符号引用。
  • 再看看运行阶段虚拟机的选择,也就是动态分派的过程。需要从invokevirtual指令多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:

    1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
    2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校
      验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回
      java.lang.IllegalAccessError异常。
    3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
    4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

    由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以三次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质


强调

静态解析的含义是这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用

  • 所以,无论是解析调用还是分派调用,都可以发生静态解析,只不过前者发生在编译加载,后者发生在运行时

解析必须有虚拟机的参与,而动态分派需要虚拟机,而静态分派依赖的是编译器

猜你喜欢

转载自blog.csdn.net/qq_32331073/article/details/80402979