本篇将主要记录invokestatic、invokespecial、invokevirtual指令的实现以及类初始化的编写,invokeinterface的实现基本类似就不赘述了。
invokestatic
该指令用来实现静态方法的调用。
|
|
在调用静态方法之前必须进行类初始化,这里先留空。而MethodInvokeLogic.invokeMethod方法是方法调用指令的公有逻辑,它包含了上一篇提到的操作数栈到局部变量表的映射逻辑,具体参看源码。
invokespecial
该指令用来调用实例构造器方法、私有方法以及父类方法。所以实现过程中需要排除解析出来的不满足条件的方法,具体详见源代码。
如果方法是父类的方法,且简单名不是
|
|
最后调用公有逻辑。
在实际的编写中本人遇到了一个bug,每个类在创建实例时会调用父类的构造器,最终也会调用java.lang.Object的
|
|
java.lang.Object的
|
|
invokevirtual
Java动态分派的原理就来自于这个指令,非静态方法的局部变量表的第0位存储着方法指向的实际对象,先获取该对象:
|
|
然后解析出方法:
|
|
最后调用公有逻辑。
类初始化
我们知道在类加载过程的连接阶段类变量只会被赋值为初始值,而且何时开始类加载《Java虚拟机规范》也没有进行强制约束,但是对于类初始化的时机,规范进行了严格规定,在下面的五种情形下如果还没有进行类初始化需要先进行类初始化:
- 遇到new、getstatic、putstatic、invokestatic四条指令;
- 对类进行反射调用;
- 初始化一个类之前,父类必须先初始化;
- 执行的主类需要先初始化;
- 使用JDK1.7动态语言支持时的某些场景;
类或接口最多只能调用一个初始化方法进行类或接口的初始化,该方法名为<clinit>
,不带参数且是void的。关于<clinit>
,甲骨文文献有这样一段话:
Class and interface initialization methods are invoked implicitly by the Java Virtual Machine; they are never invoked directly from any Java Virtual Machine instruction, but are invoked only indirectly as part of the class initialization process.
这就是为什么反编译后看不到字节码指令调用该方法,因为Java虚拟机会隐式调用处理。
在51.0以上的JDK版本该方法必须设置ACC_STATIC才能被当作是初始化方法,而之前的版本没有该要求,皆表示初始化方法。之所以可以通过查询静态方法找到<clinit>
正是因为:
|
|
接下来看看类初始化方法的具体实现:
|
|
后两个方法封装在前一个方法中。getClinitMethod方法查找当前类的<clinit>
方法,查找不到就通过clinitSuperClass方法在父类中递归查找和获取。
假设现在遇到了指令NEW,此时没有进行类初始化会去获取<clinit>
方法进行类初始化,新栈帧压入虚拟机栈执行,类初始化代码中包含return
表示不会继续完成NEW指令后续的操作,于是类初始化后需要再次执行之前的指令NEW,所以nextPC要重新设置为执行指令之前的状态:
|
|
当然这不是必须的,如果类初始化后继续执行指令内未完成的代码,就不需要设置计数器记录之前的指令状态。前提是在类初始化之前不要引用未初始化的变量,类初始化完后要确保初始化完成。
限于当前的虚拟机实现,在类初始化的五种情形中,可以先满足1、3、4三种情形。在相关指令中添加下面这段代码:
|
|
最后,因为所有的类都是java.lang.Object类的子类,所以所有的类在初始化之前会先进行java.lang.Object类的初始化,在该类中存在:
|
|
registerNatives方法是一个本地方法,在撰写本篇之时还未实现过本地方法,所以这里用一个钩子程序跳过它:
|
|
最后的测试就不附上了,达到了应有的效果。
小结
通过本篇可以很好的了解Java方法调用的实现,帮助本人进一步巩固动态分派、静态分派和类初始化的知识。
参考
2.9. Special Methods:第2.9.小节介绍了