通过字节码分析Java方法的静态分派与动态分派机制
在上一次【https://www.cnblogs.com/webor2006/p/9723289.html】中已经对Java方法的静态分派在字节码中的表现了,也就是方法重载其实是一种静态分派的体现,这次来分析一下与之对应的动态分派机制,其表现就是方法的重写多态机制,下面先来看下代码:
很显然是一个多态相关的代码,但结果会打印啥呢,看一下:
这个结果比上一节的要容易理解一些,是符合预期的,那从字节码角度如何来解释该程序的输出呢,下面详细来用jclasslib来分析一下它的字节码流程,如下:
上面是整个main方法对应字节码相关的信息,咱们一行行来分析:
其实也就是对应这句代码,生成Apple的一个实例:
看一下该助记符的官网解释:
其实就是调用了Apple的构造方法,以上三个助记符操作都是由new关键所发挥的作用,继续:
其中astore_1表示是存储在局部变量表序列号为1的变量中,那为1的局部变量是啥呢,到局部变量表中瞅一下就知道啦,如下:
也就是apple嘛,刚好跟源码对应:
以上就完成了源码的第一句代码,同样的对于第二次代码对应的助记符信息也类似,如下:
其中astore_2是保存在序列号为2的局部变量中,如下:
也就是给orange局部变量赋值了,接下来看下一句代码:
看下字节码:
看下官方解释:
而局部变量中序号为1的则就是apple如下:
接着调用Apple的test方法,但是从字节码中看到invokevirtual指令带的参数是调用的Fruit.test方法。。但是实际运行结果其实就是调用的Apple的test方法呢:
那不invokevirtual指令跟实际结果对不上了么,其实这是之后要重点学习的,这里先略过,先往下继续过流程,接着就是下一句代码了:
字节码类似的如下:
当然啦字节码中看到的是调用的Fruit.test()方法,跟结果调用的是Orange.test()结果是不符的,这个也先略过,继续看下一行代码:
对应字节码:
跟之前的操作类似,就不多说了,其中可以看到astore_1还是存在索引为1的局部变量apple,也就是引用给更新了,接下再往下看:
对应字节码如下:
最后整个方法返回:
到此,整个字节码完全分析完了,但是!!上面对于invokevirtual指令存有疑问,所以下面来解决它,先来看下这个指令的一个调用流程:
这其实是涉及到方法的动态分派,它涉及到一个很重要的概念:方法的接收者,也就是说该方法是由哪个对象所调用的,而invokevirtual字节码指令涉及到多态查找的流程,具体流程是:
1、首先要操作数栈的栈顶去寻找到栈顶的元素所指向对象的实际类型。对应于咱们的实际程序而言:
实际类型就是Apple。
2、如果在类型当中寻找到了与常量池中描述符名称相同的方法,如下:
,并且也具备一些方法的访问权限,也就是说去找看有没有实际类型的方法签名是public void test()的方法,很显然是能找到的,如下:
,那么就直接返回目标方法【对于我们程序也就是Apple.test()】的直接引用,整个过程就结束了;而如果找不到实际类型的该方法,则会从子类往父类的这种层次关系依次对各个父类重复同样的查找过程,一直到查到为止,查找到则就执行相印的方法,而如果一直找不到则就会抛出异常了。
比较方法重载(overload)与方法重写(overwrite),我们可以得到这样一个结论:方法重载是静态的,是编译期行为;方法重写是动态的,是运行期行为。而对于区分是不是方法重载和方法重写最重要的一个表现则是看方法的接收者,也就是方法的调用者是谁,像方法重载的调用者是同一个对像,如下:
因为test()方法本来就属于MyTest5类当中的。
而方法重写的调用者是不同的,如下:
也就是其调用的对象是子类的,而对于字节码是相同的符号引用而在运行期会被解析成不同的直接引用,这一点就是Java方法多态性极其重要的一个表现,如何理解?
而在运行期这两个类型会被解析Apple和Orange这两个直接引用。