这里的方法调用需要确定要调用方法是哪一个,但不包含方法的执行。
方法调用字节码指令
-
invokestatic用于调用静态方法; -
invokespecial用于调用实例构造器()方法、私有方法、父类中的方法; -
invokevirtual用于调用所有的虚方法。虚方法指的是不被final修饰的实例方法;- 静态方法、构造器方法、私有方法、final方法、父类的方法为非虚方法。
- 由于历史原因,被final修饰的非虚方法也由
invokevirtual调用。
-
invokeinterface用于调用接口方法,会在运行时确定一个实现该接口的对象; -
invokedynamic在运行时动态先解析出调用点限定符所引用的方法,然后再执行该方法。
解析调用
Java中的非虚方法(静态方法、final方法、私有方法、构造器方法、父类的方法)都使用解析调用,解析调用一定是静态的,可以在解析阶段确定唯一版本。在类加载过程中的解析阶段,JVM就会把符合解析调用条件的符号引用全部转变为明确的直接引用。
静态方法、父类的方法 为什么使用解析调用
虽然子类也可以调用父类的static方法,但是这里具体调用哪个方法在编译期就可以确定,只需要从当前类开始逐层向上寻找即可确定要调用的方法,因此也属于静态解析。
父类的方法分析逻辑与上面的static方法类似。
区分一个概念:这里只能说子类的static方法覆盖了父类的static方法,但是这里不是重写。因为重写的定义是根据对象的运行时类型调用特定的方法。
- 首先这里是根据类的类型调用的;
- 其次这里具体调用什么方法是在解析阶段确定的。
实例构造器()方法、私有方法、被final修饰的实例方法 为什么使用解析调用
这三种方法不能被子类重写,一定都使用当前类中的定义,所以可以在解析阶段确定要具体的调用版本。
为什么虚方法不能使用解析调用
JVM在执行虚方法调用时,只有在运行期才能确定具体要执行哪个函数,一个例子如下:
public class VirtualMethodDemo {
static class Human {
public void sayHello() {
System.out.println("Hello from Human");
}
}
static class Man extends Human {
@Override
public void sayHello() {
System.out.println("Hello from Man");
}
}
static class Woman extends Human {
@Override
public void sayHello() {
System.out.println("Hello from Woman");
}
}
public static void main(String[] args) {
Random random = new Random();
Human human = random.nextBoolean() ? new Man() : new Woman();
human.sayHello();
}
}
在上面的示例中,只有在运行期才能确定human到底指向Man对象还是Woman对象,进而确定函数调用sayHello指的是哪个方法。也就无法进行在解析阶段将符号引用解析为直接引用。
分派调用
首先Java具备面向对象的基本特征:封装、继承和多态。其中多态最基本的体现就是重载和重写。这两个特性和分派的对应关系如下:
- 重载:静态分派
- 重写:动态分派
静态分派(重载)
变量的静态类型和动态类型
在描述静态分派和动态分派之前,需要先区分变量的静态类型和动态类型。
/**
* 演示变量的静态类型和动态类型
*/
public class StaticTypeAndDynamicTypeDemo {
static class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
static void sayHello(Human human) {
System.out.println("Hello Human");
}
static void sayHello(Man man) {
System.out.println("Hello Man");
}
static void sayHello(Woman woman) {
System.out.println("Hello Woman");
}
public static void main(String[] args) {
// 这里human的静态类型为Human,实际类型为Man
Human human = new Man();
// human的实际类型发生变化
human = new Woman();
// human的静态类型从Human转变为Woman
sayHello((Woman)human);
}
}
如上所示,对于Human human = new Man(); 来说,human的静态类型为Human,实际类型为Man。重载时,编译器将根据变量的静态类型,而不是实际类型,来判断应该使用哪个方法。
动态分派(重写)
动态分派就是指在运行过程中,根据实际类型确定方法执行版本的分派过程。
例子
/**
* 演示变量的动态分派
*/
public class DynamicTypeDemo {
static class Human {
public void sayHello() {
System.out.println("Hello Human");
}
}
static class Man extends Human {
@Override
public void sayHello() {
System.out.println("Hello Man");
}
}
static class Woman extends Human {
@Override
public void sayHello() {
System.out.println("Hello Woman");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
字节码分析:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #7 // class DynamicTypeDemo$Man
3: dup
4: invokespecial #9 // Method DynamicTypeDemo$Man."<init>":()V
7: astore_1
8: new #10 // class DynamicTypeDemo$Woman
11: dup
12: invokespecial #12 // Method DynamicTypeDemo$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #13 // Method DynamicTypeDemo$Human.sayHello:()V
20: aload_2
21: invokevirtual #13 // Method DynamicTypeDemo$Human.sayHello:()V
24: return
其中 man.sayHello(); 和woman.sayHello();两行对应字节码的16、17和20、21行。可以看到两者都是执行invokevirtual #13 ,但是却表现出不同的行为。因此原因应该在invokevirtual内部。
invokevirtual指令的执行逻辑
-
找到操作数栈顶元素指向的对象的类型,记为C。
-
在类型C中寻找与常量中的简单名称和描述符都相符的方法。(简单名称:变量或方法的名字;描述符:变量的类型、方法的参数类型和返回值类型)
- 如果找到则进行权限校验。如果通过则返回这个方法的直接引用,查找过程结束;如果校验不通过则返回
java.lang.IllegalAccessError异常。 - 如果没找到,则按照继承关系从下向上依次对C的父类执行第二步的搜索和验证工作。
- 如果找到则进行权限校验。如果通过则返回这个方法的直接引用,查找过程结束;如果校验不通过则返回
-
如果始终没有找到合适的方法,则抛出
java.lang.AbstractMethodError异常。
分析invokevirtual指令
正因为invokevirtual 需要在运行时才能确定具体的方法的直接引用,因此也就无法在类加载的解析阶段解析符号引用。
invokevirtual指令的实现
动态分派在JVM中执行地非常频繁,每次都按照上面的逻辑搜索效率很低,因此需要优化性能。
**常用的优化手段:**在方法区创建一个虚方法表。
- 虚方法表中保存着每个虚方法的实际入口地址。如果某虚方法没有被子类重写,那么
子类方法表中该方法的入口地址=父类方法表中该类的入口地址。 - 每次调用
invokevirtual时,可以直接从虚方法表中获取到方法的入口地址,避免了频繁地执行上面的搜索过程。 - 虚方法表的构建通常在类加载过程中的链接阶段完成。在准备阶段为类变量赋零值之后,JVM会顺便将该类的虚方法表初始化完毕。
动态类型语言支持
关键特征:类型检查的主体过程实在运行期,而不是编译期。
与静态语言相比的显著差异:变量obj本身没有类型,obj的值才有类型。
静态类型语言和动态类型语言各自的优点
- 静态语言:类型错误在编译期就可以被发现,更稳定。
- 动态语言:灵活性高,开发效率高。
JVM提供的动态语言支持
invokedynamic指令java.lang.invoke包。在之前的依靠符号引用确定调用的目标方法的方式之外,提供了 MethodHandle 这种动态确定目标方法的调用机制。
参考资料
understanding-the-jvm/02-虚拟机字节码执行引擎_01-方法调用.md at master · TangBean/understanding-the-jvm
《深入理解Java虚拟机》