从java语言的层次来说,我们执行一个方法,只需要通过类的对象去调用其实例方法或者直接通过类调用其静态方,但是我们必须知道底层如何实现方法的调用。
重载、重写
重载:对于java语言来说,方法名相同,参数类型列表不同(参数的类型、参数的数目、参数的顺序)等
如果子类定义了与父类相同的方法名,参数列表不同,也是重载。
对于虚拟机来说,方法名相同,参数列表(参数类型、参数数目、参数顺序)不同、返回类型不同的方法
重写:java语言多态性的一种表现,与父类实例的方法名相同,参数列表相同,方法体不同
隐藏:这里需要提一下这个概念,与父类的重写相似,不过针对于父类的静态方法、私有方法
对于重载,在java编译阶段即已经完成。那么java编译器是如何识别?
识别阶段包括三步:
1.在不考虑基本类型自动装拆箱、可变长度参数的情况下选取方法,如果找到进行匹配,否则,继续;
2.在允许基本数据类型自动装拆箱、不考虑可变长度参数的情况下选取方法,如果找到进行匹配,否则,继续;
3.在允许基本数据类型自动装拆箱、可变长度参数的情况下选取方法。
静态绑定与动态绑定
重载在编译阶段就已经完成,重载==静态绑定?
重写在运行过程中进行,重写==动态绑定?
尽管重载的过程在编译阶段完成,但是如果子类方法重写了父类的重载方法,那么也是需要动态解析的。
静态绑定:java虚拟机可以直接识别目标方法;
动态绑定:在运行的过程中,需要知道根据调用者的动态类型来调用对应的方法。
调用指令:
invokestatic:调用静态方法
invokespecial:调用构造方法,私有方法、使用super调用父类的构造器以实例方法
invokevirtual:调用实例方法、
invokeinterface:调用接口方法
invokedybamic:调用动态方法
对于invokestatic、invokespecial,虚拟机是可以直接识别具体的目标方法。
对于invokevirtual、invokeinterface,虚拟机是要根据调用者的动态类型,确定具体的方法。特殊之处:被final修饰的方法(该方法的字节码指令为invokevirtual,因为接口的方法无法被final修饰),则不需要根据其动态类型,直接就可以确定方法。
调用指令的符号引用(根据是否为接口方法分为:接口符号引用,非接口符号引用):
在编译的过程中,我们并不知道目标方法的具体内存地址,所以编译器会根据方法所在的类作为目标定义一个符号引用来表示该方法。包括方法所在的类、方法名、签名特征。
在运行过程中,我们需要将符号引用解析为直接引用。
非接口符号引用,该符号引用指向C:
1.查找目标类C是否有符合的方法
2.搜索父类,直至Object
3.搜索直接或者间接实现的接口,是否有符合的的方法必须是非静态,非私有,如果找到多个,只能返回其中任意一个
接口符号引用,该符号引用指向I:
1.在I中查找符合条件的方法;
2.查找Object中的公有实例方法
3.在超接口查找,类似于非接口符号引用的第3步
对于静态绑定的方法调用,直接引用是一个指向方法的指针;
对于动态绑定的方法调用,直接引用是一个方法表的索引。
-------------------------------------------------------------------------------------------
那么虚方法调用是如何在java虚拟机中具体实现呢?
上面讲到:虚方法调用的指令包括两种:一种是invokevirtual,另一种是invokeinterface。
前者是调用类的非私有的实例方法;后者是调用接口的非静态方法。
在调用虚方法的时候,java虚拟机采用空间换取时间的策略,为每个类生成了一个方法表,用以快速定位具体方法。
每个方法表相当于一个数组,每个数组元素指向一个当前类或者其父类的非私有的实例方法:
一:具体的可执行的方法;二:没有相应字节码的抽象方法。
子类方法表包含父类方法表中的所有方法;子类方法表中方法的索引与父类相同方法的索引一致。
静态绑定的时候,符号引用解析后的实际引用直接指向目标方法;
动态绑定,符号引用解析后的实际引用仅仅指向方法表的索引,在调用的过程中根据调用者的具体类型来获得相应的目标方法。
使用了方法表的动态绑定与静态绑定相比:多出几个内存解引用操作
访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某索引中对应的目标方法。相比于java操作栈的初始化操作来说这几个操作的性能开销并不会影响太大。但是也是有一定的影响。优化的操作是在即时编译过程中进行的,如下:
内联缓存、方法内联
内联缓存:当java虚拟机第一次碰到调用类型的时候,会将调用者的动态类型在内存中进行缓存。下次调用的时候,会在缓存中匹配是否有一样的调用者类型,如果有的话,就直接时候用;否则,则基于方法表的动态绑定来找到对应的目标方法。
针对多态优化的情况,分为一下3种:
1.单态:指的是仅有一种状态;
2多态:指的是有限数量的状态;
3超多态:指的是更多数量的状态。
为了节省空间,java虚拟机采用的是单态内联缓存。
对于内联缓存中的内容,我们有两种选择:
一种是替换单态内联缓存的内容。一般情况下,替换内联缓存的动态类型后,接下来应该有一段时间是保持该调用者的类型,这样才可以使得缓存有效。最坏的情况下,如果我们用两种不同的调用者类型,频繁调用虚方法,则内联缓存会不断地替换缓存中的调用者的动态类型,这个时候只有“写缓存”的额外开销,却没有“用缓存的”性能提升。
另一种是劣化为超多态内联缓存。也就是相当于放弃了优化,对于不匹配的调用者的动态类型,直接通过方法表来寻找对应的目标方法,这样节省了内存的“写”开销。