调用指令的符号引用
编译过程中,我们并不知目标方法的具体内存地址。因此,Java编译器会暂时用符号引表示该目标方法。
这符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。
符号引用存储在class文件的常量池。根据目标方法是否为接口方法,这些引用可分为:
- 接口符号引用
- 非接口符号引用
// 在奸商.class的常量池中,#16为接口符号引用,指向接口方法"客户.isVIP()"。#22为非接口符号引用,指向静态方法"奸商.价格歧视()"。 $ javap -v 奸商.class ... Constant pool: ... #16 = InterfaceMethodref #27.#29 // 客户.isVIP:()Z ... #22 = Methodref #1.#33 // 奸商.价格歧视:()D ...
执行使用了符号引用的字节码前,JVM需解析这些【符号引用】并替换为【实际引用】。
对【非接口符号引用】,假定该【符号引用】所指向的类为C,则JVM按如下步骤查找:
- 在C中查找符合名字及描述符的方法
- 若没找到,搜索C的父类,直至Object类
- 若还没找到,在C所直接实现或间接实现的接口中搜索,该步搜索得到的目标方法必须是非private、非static且若目标方法在间接实现的接口中,则需满足C与该接口间无其他符合条件的目标方法。若有多个符合条件的目标方法,则返回其中任一。
所以static方法也可通过子类来调用。子类的static方法会隐藏(这不是重写)父类中的同名、同描述符的静态方法。
对于接口符号引用,假定该符号引用所指向的接口为I,则Java虚拟机会按照如下步骤进行查找。
- 在I中查找符合名字及描述符的方法。
- 如果没有找到,在Object类中的公有实例方法中搜索。
- 如果没有找到,则在I的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤3的要求一致。
经过上述解析步骤后,符号引用会被解析成实际引用:
- 对可静态绑定的方法调用,实际引用是个指向方法的指针
- 对需动态绑定的方法调用,实际引用则是个方法表的索引
总结与实践
本文介绍了Java以及Java虚拟机是如何识别目标方法的。
在Java方法的:
- 重载,方法名相同而参数类型不相同的方法间
- 重写,方法名相同&参数类型也相同的方法间
JVM识别方法的方式除了方法名和参数类型,还有返回类型。
JVM的:
- 静态绑定:在解析时便能够直接识别目标方法的情况
- 动态绑定,需在运行过程中根据调用者的动态类型来识别目标方法的情况。由于Java编译器已区分重载方法,因此可认为JVM不存在重载
在class文件中,Java编译器会用符号引用指代目标方法。在执行调用指令前,它所附带的符号引用需要被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。
Java的重写与Java虚拟机中的重写并不一致,但编译器会通过生成桥接方法来弥补。
参考
- [1] https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html
- [2] https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html
- [3] https://wiki.openjdk.java.net/display/HotSpot/VirtualCalls
- [4] https://wiki.openjdk.java.net/display/HotSpot/InterfaceCalls