JVM–基础–17–方法调用:解析与分派
1、方法调用
- 方法调用并不等于方法的执行
- 方法调用阶段唯一的任务就是确定被调用方法的版本(考虑多态情况)。
2、方法解析
方法解析:在类加载的方法解析阶段,会将方法一部分的方法符号引用转化为直接引用。
这种方法解析的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不变的。换句话说,调用方法的版本在编译期时就必须确定下来。
2.1、这类方法主要包括:
- 静态方法:与类直接关联
- 私有方法:后者在外部不可被访问
- 这两种方法都不能通过继承或者别的方式重写其他版本,因此他们都适合在类的加载阶段进行解析。
2.2、在Java虚拟机里提供了5种方法调用字节码指令
1. invokestatic:调用静态方法。
2. invokespecial:调用实例构造器< init>方法,私有方法和父类方法。
3. invokevirtual:调用所有的虚方法。()
4. invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
5. invokedynamic:现在运行时动态解析调用点限定符所引用的方法,然后再执行该方法。
只要是能被invokestatic和jinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本
符合这个条件的方法有
1. 有静态方法
2. 私有方法
3. 实例构造器
4. 父类方法
他们类加载的时候就会把符号引用解析为该方法的直接引用。 这些都可以称为非虚方法,与之对应的就是虚方法。
Java种的虚方法除了使用invokestatic,invokespecial调用的方法之外还有一种,就是被final修饰的方法。再Java语言规范中明确说明了final方法是一种非虚方法。
3、分派
- 解析调用是一个静态的过程,在编译期间就完全确定,不会延迟到运行期再去完成。
- 分派调用:可以是静态的也可以是动态的。根据分派中数量可分为单分派和多分派。这两类分派又可两两组合成:静态单分派,静态多分派,动态单分派和动态多分派
- 分派体现了Java的多态性,如"重载"和"重写"。
3.1、静态分派(重载)
3.1.1、代码
//静态分派
public class Test {
static abstract class Human{
}
static class Man extends Human{
}
static class Woman extends Human{
}
public void sayhello(Human Human){
System.out.println("Human");
}
public void sayhello(Man Man){
System.out.println("Man");
}
public void sayhello(Woman Woman){
System.out.println("Woman");
}
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
3.1.2、对应的字节码
F:\>javap -v Test.class
Classfile /F:/Test.class
Last modified 2019-7-25; size 807 bytes
MD5 checksum bf1ef72fc4f554f5db2536efaae70880
Compiled from "Test.java"
public class Test
SourceFile: "Test.java"
InnerClasses:
static #15= #9 of #11; //Woman=class Test$Woman of class Test
static #17= #7 of #11; //Man=class Test$Man of class Test
static abstract #19= #18 of #11; //Human=class Test$Human of class Test
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#32 // java/lang/Object."<init>":()V
#2 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #19 // Human
#4 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #17 // Man
#6 = String #15 // Woman
#7 = Class #37 // Test$Man
#8 = Methodref #7.#32 // Test$Man."<init>":()V
#9 = Class #38 // Test$Woman
#10 = Methodref #9.#32 // Test$Woman."<init>":()V
#11 = Class #39 // Test
#12 = Methodref #11.#32 // Test."<init>":()V
#13 = Methodref #11.#40 // Test.sayhello:(LTest$Human;)V
#14 = Class #41 // java/lang/Object
#15 = Utf8 Woman
#16 = Utf8 InnerClasses
#17 = Utf8 Man
#18 = Class #42 // Test$Human
#19 = Utf8 Human
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 sayhello
#25 = Utf8 (LTest$Human;)V
#26 = Utf8 (LTest$Man;)V
#27 = Utf8 (LTest$Woman;)V
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 SourceFile
#31 = Utf8 Test.java
#32 = NameAndType #20:#21 // "<init>":()V
#33 = Class #43 // java/lang/System
#34 = NameAndType #44:#45 // out:Ljava/io/PrintStream;
#35 = Class #46 // java/io/PrintStream
#36 = NameAndType #47:#48 // println:(Ljava/lang/String;)V
#37 = Utf8 Test$Man
#38 = Utf8 Test$Woman
#39 = Utf8 Test
#40 = NameAndType #24:#25 // sayhello:(LTest$Human;)V
#41 = Utf8 java/lang/Object
#42 = Utf8 Test$Human
#43 = Utf8 java/lang/System
#44 = Utf8 out
#45 = Utf8 Ljava/io/PrintStream;
#46 = Utf8 java/io/PrintStream
#47 = Utf8 println
#48 = Utf8 (Ljava/lang/String;)V
{
public Test();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
line 13: 4
public void sayhello(Test$Human);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Human
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 18: 0
line 19: 8
public void sayhello(Test$Man);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String Man
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 22: 0
line 23: 8
public void sayhello(Test$Woman);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String Woman
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 26: 0
line 27: 8
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #7 // class Test$Man
3: dup
4: invokespecial #8 // Method Test$Man."<init>":()V
7: astore_1
8: new #9 // class Test$Woman
11: dup
12: invokespecial #10 // Method Test$Woman."<init>":()V
15: astore_2
16: new #11 // class Test
19: dup
20: invokespecial #12 // Method "<init>":()V
23: astore_3
24: aload_3
25: aload_1
26: invokevirtual #13 // Method sayhello:(LTest$Human;)V
29: aload_3
30: aload_2
31: invokevirtual #13 // Method sayhello:(LTest$Human;)V
34: return
LineNumberTable:
line 32: 0
line 33: 8
line 35: 16
line 36: 24
line 37: 29
line 38: 34
3.1.3、分析
字节码中已经确定了方法的接收者是main和方法的版本是sayhello(Human Human),所以我们在运行代码的时候,会输出Human。
java编译器实际类在重载的时候是通过参数的静态类型来确定使用哪个重载的版本的。所以这里在字节码中,选择了 sayhello(Human Human) 作为调用目标并把这个方法的符号引用写到main方法的几个invokevirtual指令的参数里面。
3.1.4、总结:
- 依赖静态类型来定位方法执行的版本的分派动作称为静态分派。
- 静态分派的典型应用是方法重载,
- 静态分派发生在编译期间,动作是由编译器发出的。
3.1.5、注意:
编译器能确定出方法的重载版本,但在很多的时候,这个版本并不一定是唯一的,jvm会做出妥协,选择并确认一个版本
3.2、动态分派(重写)
动态分派:运行期根据实际类型来判断方法的执行版本的分派过程。
3.2.1、代码
3.2.2、对应的字节码
3.2.3、分析
从字节码来看,这两行代码是一样的。调用了同一个类的同一个方法,,那为什么他们最后的输出却不一样?
这里的原因其实要从invokevirtual的多态查找开始说起,invokevirtual指令运行时的解析过程大概如下:
1. 找到操作数栈的栈顶元素所指向的对象的实际类型,记作C
2. 如果在类型C中找到与描述符和简单名称都相符的方法,则进行访问权限校验。通过则放回这个方法的直接引用,否则返回illegalaccesserror 。
3. 否则,则按照继承关系从下住上依次对C的父类进行步骤2的查找。
4. 如果始终没有找到合适的方法,则跑出AbstractMethodError异常。
由于invokevirtual指令在执行的第1步就对运行的时候的接收者的实际类型进行查找,所以上面两次调用的invokevirtual指令都能成功找到实际类型的sayhello()方法,然后把类方法的符号引用解析到不同的直接引用上面,这也是重写的体现。