Java虚拟机之方法引用实现原理(下)

本篇将主要记录invokestatic、invokespecial、invokevirtual指令的实现以及类初始化的编写,invokeinterface的实现基本类似就不赘述了。

invokestatic

该指令用来实现静态方法的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class INVOKE_STATIC extends Index16Instruction {
@Override
public void execute(StackFrame_ frame) {
RuntimeConstantPool_ cp = frame.getMethod_().getClass_().getRuntimeConstantPool();
MethodRef methodRef = (MethodRef) (cp.getConstant(index).getVal());
Method_ resolvedMethod = methodRef.resolvedMethod();
if (!resolvedMethod.isStatic()) {
throw new IncompatibleClassChangeError();
}
// todo 类初始化判断,类没有初始化要进行初始化
MethodInvokeLogic.invokeMethod(frame, resolvedMethod);
}
}

在调用静态方法之前必须进行类初始化,这里先留空。而MethodInvokeLogic.invokeMethod方法是方法调用指令的公有逻辑,它包含了上一篇提到的操作数栈到局部变量表的映射逻辑,具体参看源码。

invokespecial

该指令用来调用实例构造器方法、私有方法以及父类方法。所以实现过程中需要排除解析出来的不满足条件的方法,具体详见源代码。

如果方法是父类的方法,且简单名不是,则解析出父类的方法作为解析方法,代码如下:

1
2
3
4
5
6
7
8
9
10
// JDK 1.0.2之后编译的类的这个标志必须为真 见《深入理解Java虚拟机》P173
if (currentClass.isSuper()
// 解析出来的类是父类
&& resolvedClass.isSuperClassOf(currentClass)
// 解析的是父类的方法而且不是<init>构造器方法
&& !(resolvedMethod.getName().equals("<init>"))) {
// 通过简单名和描述符找出父类满足条件的方法
resolvedMethod = MethodLookup.lookupMethodInClass(currentClass.getSuperClass(),
methodRef.getName(), methodRef.getDescriptor());
}

最后调用公有逻辑。

在实际的编写中本人遇到了一个bug,每个类在创建实例时会调用父类的构造器,最终也会调用java.lang.Object的,具体字节码见反编译结果:

1
2
3
4
5
6
7
8
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

java.lang.Object的方法Code属性内操作数栈大小为0,所以操作数栈在初始化的过程中需要添加是否大于0的判断:

1
2
3
4
5
6
7
8
public OperandStack_(int maxStack) {
if (maxStack > 0) {
slots = new Slot_[maxStack];
for (int i = 0; i < slots.length; i++) {
slots[i] = new Slot_();
}
}
}

invokevirtual

Java动态分派的原理就来自于这个指令,非静态方法的局部变量表的第0位存储着方法指向的实际对象,先获取该对象:

1
2
3
4
5
// 被调用方法局部变量表第0号索引代表this,是从调用该方法的帧的操作数栈倒入的
Instance_ ref = frame.getOperandStack().getRefFromTop(resolvedMethod.getArgSlotCount());
if (ref == null) {
throw new NullPointerException();
}

然后解析出方法:

1
2
resolvedMethod = MethodLookup.lookupMethodInClass(ref.getClass_(),
methodRef.getName(), methodRef.getDescriptor());

最后调用公有逻辑。

类初始化

我们知道在类加载过程的连接阶段类变量只会被赋值为初始值,而且何时开始类加载《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>正是因为:

1
2
3
public boolean isStatic() {
return 0 != (accessFlags & AccessFlags.ACC_STATIC);
}

接下来看看类初始化方法的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 类初始化方法
public static void clinitClass(Thread_ thread, Class_ class_) {
getClinitMethod(thread, class_);
clinitSuperClass(thread, class_);
class_.setClinitedFlag(true);
}
// 获取<clinit>方法,一旦获取到就生成栈帧压入虚拟机栈中
private static void getClinitMethod(Thread_ thread, Class_ class_) {
Method_ clinit = class_.getStaticMethod("<clinit>", "()V");
// 从当前的类查找类初始化器,如果不存在就要考虑在父类中查找了,
// 后者相当于递归调用,将在clinitSuperClass方法中实现
if (clinit != null) {
StackFrame_ clinitFrame = new StackFrame_(thread, clinit);
thread.pushStackFrame_(clinitFrame);
}
}
// 在父类中递归查找和获取<clinit>方法
private static void clinitSuperClass(Thread_ thread, Class_ class_) {
if (!class_.isInterface()) {
Class_ superClass = class_.getSuperClass();
if (superClass != null && !superClass.getClinitFlag())
clinitClass(thread, superClass);
}
}

后两个方法封装在前一个方法中。getClinitMethod方法查找当前类的<clinit>方法,查找不到就通过clinitSuperClass方法在父类中递归查找和获取。

假设现在遇到了指令NEW,此时没有进行类初始化会去获取<clinit>方法进行类初始化,新栈帧压入虚拟机栈执行,类初始化代码中包含return表示不会继续完成NEW指令后续的操作,于是类初始化后需要再次执行之前的指令NEW,所以nextPC要重新设置为执行指令之前的状态:

1
2
3
4
// 修改frame中的nextPC(用以重置线程pc)
public static void revertNextPc(StackFrame_ frame) {
frame.setNextPC(frame.getThread_().getPc());
}

当然这不是必须的,如果类初始化后继续执行指令内未完成的代码,就不需要设置计数器记录之前的指令状态。前提是在类初始化之前不要引用未初始化的变量,类初始化完后要确保初始化完成。

限于当前的虚拟机实现,在类初始化的五种情形中,可以先满足1、3、4三种情形。在相关指令中添加下面这段代码:

1
2
3
4
5
6
// 类初始化
if (!class_.getClinitFlag()) {
Clinit.revertNextPc(frame);
Clinit.clinitClass(frame.getThread_(), class_);
return;
}

最后,因为所有的类都是java.lang.Object类的子类,所以所有的类在初始化之前会先进行java.lang.Object类的初始化,在该类中存在:

1
2
3
static {
registerNatives();
}

registerNatives方法是一个本地方法,在撰写本篇之时还未实现过本地方法,所以这里用一个钩子程序跳过它:

1
2
3
if (method.isNative() && method.getName().equals("registerNatives")) {
thread.popStackFrame_();
}

最后的测试就不附上了,达到了应有的效果。

小结

通过本篇可以很好的了解Java方法调用的实现,帮助本人进一步巩固动态分派、静态分派和类初始化的知识。

参考

2.9. Special Methods:第2.9.小节介绍了