TOC
JVM是如何执行方法调用的?(上)
一、重载与重写
同一个类中出现多个名字相同,并且参数类型相同的方法,那么他们的参数类型必须不同。这些方法之间的关系,我们称为重载。
小知识:这个限制可以通过字节码工具绕开。
也就是说,在编译完成之后,我们可以再向 class 文件中添加方法名和参数类型相同,而返回类型不同的方法。
当这种包括多个方法名相同、参数类型相同,而返回类型不同的方法的类,出现在 Java 编译器的用户类路径上时,它是怎么确定需要调用哪个方法的呢?
当前版本的 Java 编译器会直接选取第一个方法名以及参数类型匹配的方法。
并且,它会根据所选取方法的返回类型来决定可不可以通过编译,以及需不需要进行值转换等。
二、JVM 的静态绑定和动态绑定
1.Java虚拟机是怎么识别方法的。
Java虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descirptor) 方法描述符,它是由方法的参数类型以及返回类型所构成。在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么Java虚拟机会在类的验证阶段报错。 Java虚拟机中关于方法重写的判断同样基于方法描述符。如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这个方法的参数类型以及返回类型一致,Java虚拟机才会判定为重写。
绑定——将一个方法的调用与方法所在的类关联起来。 · 由于对重载方法的区分在编译阶段已经完成,我们可以认为Java虚拟机不存在重载这一概念。在某些文章中,重载也被称为静态绑定(static binding),或者编译时多态(compile-time-polymorphism);而重写则被称为动态绑定(dynamic binding)
2.具体来说,Java字节码中与调用相关的指令共有五种。
- invokestatic:用于调用静态方法。
- invokespecial:用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
- invokevirtual:用于调用非私有实例方法。
- invokeinterface:用于调用接口方法。
invokedynamic:用于调用动态方法。
interface 客户 { boolean isVIP(); } class 商户 { public double 折后价格 (double 原价, 客户 某客户) { return 原价 * 0.8d; } } class 奸商 extends 商户 { @Override public double 折后价格 (double 原价, 客户 某客户) { if (某客户.isVIP()) { // invokeinterface return 原价 * 价格歧视 (); // invokestatic } else { return super. 折后价格 (原价, 某客户); // invokespecial } } public static double 价格歧视 () { // 咱们的杀熟算法太粗暴了,应该将客户城市作为随机数生成器的种子。 return new Random() // invokespecial .nextDouble() // invokevirtual + 0.8d; } }
三、调用指令的符号引用
在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java编译器会暂时用符号引用来表示该目标方法。 这一符号引用包含目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。
符号引用存储在class文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。
利用“javap -v”打印某个类的常量池
// 在奸商.class 的常量池中,
//#16 为接口符号引用,指向接口方法 " 客户.isVIP()"。
//而 #22 为非接口符号引用,指向静态方法 " 奸商. 价格歧视 ()"。
$ javap -v 奸商.class ...
Constant pool:
...
#16 = InterfaceMethodref #27.#29 // 客户.isVIP:()Z
...
#22 = Methodref #1.#33 // 奸商. 价格歧视:()D
...
上一篇中我曾提到过,在执行使用了符号引用的字节码前,Java虚拟机需要解析这些符号引用,并替换为实际引用。
对于非接口符号引用,假定该符号引用所指向的类为C,则Java虚拟机会按照如下步骤进行查找。
- 在C中查找符号名字及描述符的方法
- 如果没有找到,在C的父类中继续搜索,直至Object类。
- 如果没有找到,在C所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足C与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。
从这个解析算法可以看出,静态方法也可以通过子类来调用。此外,子类的静态方法会隐藏(注意与重写区分)父类中的同名、同描述符的静态方法。
对于接口符号引用,假定该符号引用所指向的接口为I,则Java虚拟机会按照如下步骤进行查找。
- 在I中查找符合名字及描述符的方法。
- 如果没有找到,在Object类中的公有实例方法中搜索。
- 如果没有找到,则在I的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤3的要求一致。
经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。