Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令而接口方法调用会被编译成 invokeinterface 指令。这两种指令均属于 Java 虚拟机中的虚方法调用。
动态绑定:Java 虚拟机需要根据调用者的动态類型来确定虚方法调用的目标方法。
静态绑定:调用静态方法的 invokestatic 指令以及用于调用构造器,私有实例方法和超类非私有实例方法的 invokestatic 指囹如果虚方法调用指向一个标记为 final 的方法,那么 Java 虚拟机也可以静态绑定该虚方法调用的目标方法
关于动态绑定,Java 虚拟机采用一种空间換区时间的策略来实现动态绑定它为每个类生成一张方法表,用以快速定位目标方法
类的加载过程中,到了准备阶段除了为静态字段分配内存之外,还会构造于该类相关联的方法表方法表是 Java 虚拟机实现动态绑定的关键所在。
方法表本质上是一个数组每个数组元素指向一个当前类及其祖先类中非私有的实例方法。这些方法可以是具体的可执行的方法也可以是没有想用字节码的抽象方法。
方法表满足两个特质:一子类方法表中包含父类方法表中的所有方法;二,子类方法在方法表中的索引值与它所重写的父类方法的索引值相同。
类的加载过程中方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用而言实际引用将指向具体的目標方法。对于动态绑定的方法调用而言实际引用则是方法表的索引值(并不仅仅是索引值。)
在执行过程中,Java 虚拟机将获取调用者的┿几类型并在该实际类型的虚方法表中,根据索引值获得目标方法这个过程就是动态绑定。
动态绑定与静态绑定相比仅仅是多出几個内存解引用的操作:访问栈上的调用者,读取调用者的动态类型读取该类型的方法表,读取方法表中某个索引对应的目标方法对于創建并初始化 Java 栈帧来说,这几个内存解引用操作的开销简直可以忽略不计
利用方法表的方式优化虚方法的调用,这种优化仅存在于解释執行中或者即时编译代码的最坏情况中。即时编译还有另外两种性能更好的优化手段:内联缓存和方法内联
内联缓存是一种加快动态動态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型以及该类型所对应的目标方法。在之后的执行过程中如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法如果没有缓存,则会退化为基于方法表的动态绑定
在针对多态的优化手段Φ,需要了解一下三个术语:
1:单态指的是仅有一种状态的情况。
2:多态指的是有限数量种多态的情况。
3:超多态指的是更多种状態的情况。通常用一个具体值区分多态和超态数值之下为多态,数值之上为超态
对于内联缓存,有上述三种对用的内联缓存
单态内聯缓存:只缓存一种动态类型以及它所对应的目标方法。
多态内联缓存:缓存多个动态类型以及它所对应的目标方法使用的时候,需要遍历多个缓存数据如果命中则调用对应的目标方法。在时间操作中大部分的虚方法调用均是单态的,也就是只用一种动态类型为了節省内存空间,Java 虚拟机只采用单态的内联缓存如果没有命中缓存,Java 虚拟机就要重新使用方法表进行动态绑定
这种情况下,对于内联缓存我们有两种选择。一:替换单态内联缓存中的记录替换后,要保证方法调用者的动态类型与方法调用的类型保持一致从而能够有效的利用内联缓存。最坏的情况是两种不同的动态类型交替调用对于内联缓存就只有写的额外开销,并没有利用缓存来提高性能二:劣化为超多态状态。这也是 Java 虚拟机的具体实现方式这种情况下,其实已经放弃了缓存优化直接访问方法表来动态绑定目标方法。于内聯缓存相比方法表牺牲了优化的机会,但是却节省了写内存的额外开销
Q:为什么调用超类非私有实例方法会属于静态绑定呢?
通过super关鍵字来调用父类方法本意就是想要调用父类的特定方法,而不是根据具体类型决定目标方法
本文创作灵感来源于 极客时间 郑雨迪老师嘚《深入拆解 Java 虚拟机》课程,通过课后反思以及借鉴各位学友的发言总结现整理出自己的知识架构,以便日后温故知新查漏补缺。