一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情
本系列专栏:JVM专栏
前言
上一篇文章,我们说了Java语言和JVM对于重载和重写是不一样的,并且Java编译器通过生成桥接方法来弥补这一不同点,那本章内容就直接来说Java字节码中不同的方法调用是如何区分的。
正文
静态绑定和动态绑定
对于不同的方法,前面说了有的方法在解析Java代码时便能够识别目标方法,而有的方法必须要在运行时根据调用者的动态类型来识别目标方法,比如上一篇文章说的重写的情况,只有在运行时,发现调用类型是Student,这时才可以去调用Student中的重写方法(而不是People中的方法),前者就是静态绑定,而后者就是动态绑定。
所以根据逻辑,Java字节码中关于方法调用的一共有5种:
-
invokestatic:用于调用静态方法。这种方法有什么特点呢,调用它是不需要类的实例的,即在操作栈中,只需要弹出该静态方法所需的参数即可。
-
invokespecial:用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或者构造器,和所实现接口的默认方法。这种方法的特点就是能够直接找到,不需要运行时才能找到目标方法。
-
invokevirtual:用于调用非私有实例方法。这种方法就存在被重写的情况,只能等到运行时才能找到目标方法。
-
invokeinterface:用于调用接口方法。同样这种方法也需要等到运行时,才能找到目标方法。
-
invokedynamic:用于调用动态方法,该调用比较复杂,后面单独再说。
由定义可知,这里的invokevertual和invokeinterface需要在JVM运行过程中,根据调用者的动态类型来确定目标调用方法。
简单例子
我们还是通过一个简单的例子,来验证一下上面所说的调用指令,是否符合预期。
下面定义了3个类:
//客户接口,是否是VIP
public interface ICustom {
boolean isVIP();
}
//正常超市
public class Market {
//打折价格
public double OffPrice(double price,ICustom customer){
return price * 0.8;
}
}
//假超市,会对VIP进行特殊加价
public class FakeMarket extends Market{
//判断是否是VIP
public double OffPrice(double price,ICustom customer){
if (customer.isVIP()){
return fakeOffPrice(price);
}else {
return super.OffPrice(price,customer);
}
}
//价格更高
public static double fakeOffPrice(double price){
return price * 0.9;
}
}
上面定义了一个简单的逻辑,可以进行查看,我们来看一下FakeMarket类的字节码:
public class com.wayeal.arithmeticapp.testInvoke.FakeMarket extends com.wayeal.arithmeticapp.testInvoke.Market
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // com/wayeal/arithmeticapp/testInvoke/FakeMarket
super_class: #8 // com/wayeal/arithmeticapp/testInvoke/Market
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #8.#20 // com/wayeal/arithmeticapp/testInvoke/Market."<init>":()V
#2 = InterfaceMethodref #21.#22 // com/wayeal/arithmeticapp/testInvoke/ICustom.isVIP:()Z
#3 = Methodref #7.#23 // com/wayeal/arithmeticapp/testInvoke/FakeMarket.fakeOffPrice:(D)D
#4 = Methodref #8.#24 // com/wayeal/arithmeticapp/testInvoke/Market.OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
#5 = Double 0.9d
#7 = Class #25 // com/wayeal/arithmeticapp/testInvoke/FakeMarket
#8 = Class #26 // com/wayeal/arithmeticapp/testInvoke/Market
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 OffPrice
#14 = Utf8 (DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
#15 = Utf8 StackMapTable
#16 = Utf8 fakeOffPrice
#17 = Utf8 (D)D
#18 = Utf8 SourceFile
#19 = Utf8 FakeMarket.java
#20 = NameAndType #9:#10 // "<init>":()V
#21 = Class #27 // com/wayeal/arithmeticapp/testInvoke/ICustom
#22 = NameAndType #28:#29 // isVIP:()Z
#23 = NameAndType #16:#17 // fakeOffPrice:(D)D
#24 = NameAndType #13:#14 // OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
#25 = Utf8 com/wayeal/arithmeticapp/testInvoke/FakeMarket
#26 = Utf8 com/wayeal/arithmeticapp/testInvoke/Market
#27 = Utf8 com/wayeal/arithmeticapp/testInvoke/ICustom
#28 = Utf8 isVIP
#29 = Utf8 ()Z
{
public com.wayeal.arithmeticapp.testInvoke.FakeMarket();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/wayeal/arithmeticapp/testInvoke/Market."<init>":()V
4: return
LineNumberTable:
line 4: 0
public double OffPrice(double, com.wayeal.arithmeticapp.testInvoke.ICustom);
descriptor: (DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
flags: (0x0001) ACC_PUBLIC
Code:
stack=4, locals=4, args_size=3
0: aload_3
1: invokeinterface #2, 1 // InterfaceMethod com/wayeal/arithmeticapp/testInvoke/ICustom.isVIP:()Z
6: ifeq 14
9: dload_1
10: invokestatic #3 // Method fakeOffPrice:(D)D
13: dreturn
14: aload_0
15: dload_1
16: aload_3
17: invokespecial #4 // Method com/wayeal/arithmeticapp/testInvoke/Market.OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
20: dreturn
LineNumberTable:
line 8: 0
line 9: 9
line 11: 14
StackMapTable: number_of_entries = 1
frame_type = 14 /* same */
public static double fakeOffPrice(double);
descriptor: (D)D
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: dload_0
1: ldc2_w #5 // double 0.9d
4: dmul
5: dreturn
LineNumberTable:
line 17: 0
}
相信大家如果阅读过前面文章,对于字节码的阅读已经不费事了,我们直接来看OffPrice方法,其中#1行代码:
1: invokeinterface #2, 1 // InterfaceMethod com/wayeal/arithmeticapp/testInvoke/ICustom.isVIP:()Z
这里是调用接口的isVIP方法,发现这里是通过invokeinterface方法调用,对于#10行代码:
10: invokestatic #3 // Method fakeOffPrice:(D)D
这里是调用类自己的静态方法,使用invokestatic指令,对于#17行代码:
17: invokespecial #4 // Method com/wayeal/arithmeticapp/testInvoke/Market.OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
调用父类的方法,这里是通过super来调用,根据规则使用invokespecial指令。
可以发现这里调用的指令和我们预期是相符的。
调用指令的符号引用
在前面文章介绍字节码时,我们说过在字节码的常量池中保存的是常量值和符号引用,对于方法而言,在编译过程中,我们并不知道目标方法的具体内存地址,因此Java编译器会暂时用符号引用来表示目标方法。
方法的符号引用就包括目标方法所在的类或者接口的名字,以及目标方法的方法名和方法描述符,比如下面方法的符号引用:
#2 = InterfaceMethodref #21.#22 // com/wayeal/arithmeticapp/testInvoke/ICustom.isVIP:()Z
#3 = Methodref #7.#23 // com/wayeal/arithmeticapp/testInvoke/FakeMarket.fakeOffPrice:(D)D
#4 = Methodref #8.#24 // com/wayeal/arithmeticapp/testInvoke/Market.OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
那么在前面加载class文章里我们说过,在执行步骤前,要对这些符号引用进行解析,并且替换为实际引用,那JVM如何去查找呢。
非接口符号引用
假设一个符号引用所指向的类为C,则JVM会按照下面步骤进行查找:
-
在C中查找符合名字以及描述符的方法。
-
如果没有找到,在C的父类中继续搜索,知道Object类。
-
如果没有找到,在C所直接实现或者间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。
从这个解析算法可以看出静态方法也可以通过子类来调用,因此子类的静态方法会隐藏父类中同名、同描述符的静态方法。
接口符号引用
假设一个符号引用所指向的接口为I,则JVM会按照下面步骤查找:
-
在I中查找符合名字以及描述符的方法。
-
如果没有找到,在Object类中的公有实例方法中搜索。
-
如果没有找到,则在I的超接口中搜索,同样是非私有、非静态的。
经过上述解析步骤后,符号引用就会被解析为实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。具体什么是方法表,我们在下篇文章仔细来说明。
具体来说就是对于invokestatic和invokespecial能找到对应方法的指针,而invokevirtual和invokeinterface找到的是方法表中的一个索引。
总结
本节内容主要是2个部分,重点介绍了因为Java语言和JVM对于重写的定义不一样导致的方法调用也不一样,而对于常见的4种方法调用也分为2种,静态绑定在解析阶段就可以找到实际方法的指针,而动态绑定只能指向一个方法表的索引,在运行时才能确定具体调用的方法。