两个概念
方法调用:确定被调用方法的版本
- 解析:类加载期间确定,由符号引用转为直接引用,
invokestatic(调用静态方法)
invokespecial(调用实例构造器init方法、私有方法、父类中的方法) - 分派:运行期间确定,确定目标方法的直接引用
invokevirtual(调用虚方法)
invokeinterface(调用接口方法,在运行时确定一个实现该接口的对象)
invokedynamic(运行时动态解析出调用点限定符所引用的方法,然后再执行该方法)
方法执行:由执行引擎执行此方法
解析
解析的定义
在类加载的解析阶段,会将其中的一部分符号引用转为直接饮用,这种解析的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。也就是说,调用目标在程序代码写好、编译器进行编译的那一刻就已经确定下来。这就是方法调用中的解析
在Java当中符合“编译期可知,运行期不可变”的方法,主要有静态方法和私有方法两大类,因为静态方法与类型直接关联,私有方法在外部不可见。这两种方法的共同特点决定了它们适合在类加载阶段进行解析。
被invokestatic和invokespecial指令调用的方法都可以在解析阶段中唯一确定调用版本。具体包含静态方法、私有方法、实例构造器、父类方法这4种,以及被final修饰的方法,这五种方法调用会在类加载的时候把符号引用解析为该方法的直接引用。称为非虚方法。
final方法由于历史设计的原因,也是使用invokevirtual指令来调用的,但是因为它无法被覆盖、也没有其他版本的可能,所以也可以放在解析的过程中进行调用
分派
重载:静态分派
重写:动态分派
静态类型和实际类型
Human human = new Man()
其中Human称为变量的“静态类型”,或者叫“外观类型”
其中Man则被称为变量的“实际类型”或者叫“运行时类型”
其中二者的区别为:静态类型在编译期是可知的、实际类型的变化在运行期才可以确定。
静态分派:重载
public class StaticDispatch {
// 重载方法,按照静态类型判定具体选择哪个
public void sayHello(Human guy) {
System.out.println("hello guy");
}
public void sayHello(Man man) {
System.out.println("hello man");
}
public void sayHello(Woman woman) {
System.out.println("hello woman");
}
public static void main(String[] args) {
StaticDispatch staticDispatch = new StaticDispatch();
// 静态类型Human 实际类型Man
Human man = new Man();
Human woman = new Woman();
// 根据静态类型Human确定了调用方法的版本
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
}
}
abstract class Human {}
class Man extends Human{}
class Woman extends Human{}
虚拟机在重载时是通过参数的静态类型来作为判定依据的,而不是实际类型。
所以Javac编译器就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human human)作为调用的目标
动态分派:重写
public class DynamicDispatch {
public static void main(String[] args) {
Human human = new Woman();
Human woman = new Man();
human.sayHello();
woman.sayHello();
}
}
abstract class Human {
protected abstract void sayHello();
}
class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("i am woman");
}
}
class Man extends Human {
@Override
protected void sayHello() {
System.out.println("i am man");
}
}
其方法的调用指令都是通过字节码指令invokevirtual来实现
invokevirtual指令的运行时解析过程大致分为以下几步
- 找到操作数栈顶的第一个元素所指向的对象的实际类型 C
- 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限验证,如果通过则返回这个方法的直接引用,不通过则返回java.lang.IllegalAccessError异常。
- 如果在类型 C 中没有找到与常量中的描述符和简单名称都相符的方法,则按照继承关系从下往上对 C 的各个父类进行第二步的搜索和验证过程
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
正是因为invokevirtual在两次调用中invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,而是会接着得到确定接受者的实际类型,然后根据实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。这 种 运行时根据实际类型确定方法执行版本的分派过程就称之为动态分派。而多态性的根源就在于虚方法调用指令invokevirtual的执行逻辑,所以对方法有效,而对字段无效。
静态多分派与动态单分派
静态多分派:选择目标方法的依据有两点:静态类型,方法参数
动态单分派:选择目标方法的依据只有一点:实际类型
虚拟机动态分派的实现
动态分派是执行非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在接收者类型的方法元数据中搜索合适的目标方法,因此Java虚拟机实现基于执行性能考虑的,会为每个类型在方法区当中建立一个虚方法表以及接口方法表来代替元数据查找以提高性能。
虚方法中存放着各个方法的实际入口地址,即当前类的方法可能使用的是其他类的类型数据。如果某个方法在子类中没有被重写,则子类和父类的虚方法表的方法地址入口相同,均指向父类方法的地址入口。