第4章实现了Java虚拟机栈、帧等运行时数据区,为方法的执行打好了基础。第5章实现了一个简单的解释器和150多条指令,已经可以执行单个方法。第6章实现了方法区,为方法调用扫清了障碍。本章将实现方法调用和返回,在此基础上,还会讨论类和对象的初始化。
文章目录
代码目录
ZYX-demo-jvm-07
├── pom.xml
└── src
└── main
│ └── java
│ └── org.ZYX.demo.jvm
│ ├── classfile
│ │ ├── attributes {BootstrapMethods/Code/ConstantValue...}
│ │ ├── constantpool {CONSTANT_TAG_CLASS/CONSTANT_TAG_FIELDREF/CONSTANT_TAG_METHODREF...}
│ │ ├── ClassFile.java
│ │ ├── ClassReader.java
│ │ └── MemberInfo.java
│ ├── classpath
│ │ ├── impl
│ │ │ ├── CompositeEntry.java
│ │ │ ├── DirEntry.java
│ │ │ ├── WildcardEntry.java
│ │ │ └── ZipEntry.java
│ │ ├── Classpath.java
│ │ └── Entry.java
│ ├── instruction
│ │ ├── base
│ │ │ ├── BytecodeReader.java
│ │ │ ├── ClassInitLogic.java
│ │ │ ├── Instruction.java
│ │ │ ├── InstructionBranch.java
│ │ │ ├── InstructionIndex8.java
│ │ │ ├── InstructionIndex16.java
│ │ │ ├── InstructionNoOperands.java
│ │ │ └── MethodInvokeLogic.java
│ │ ├── comparisons
│ │ ├── constants
│ │ ├── control
│ │ ├── conversions
│ │ ├── extended
│ │ ├── loads
│ │ ├── math
│ │ ├── references
│ │ │ ├── CHECK_CAST.java
│ │ │ ├── GET_FIELD.java
│ │ │ ├── GET_STATIC.java
│ │ │ ├── INSTANCE_OF.java
│ │ │ ├── INVOKE_INTERFACE.java
│ │ │ ├── INVOKE_SPECIAL.java
│ │ │ ├── INVOKE_STATIC.java
│ │ │ ├── INVOKE_VIRTUAL.java
│ │ │ ├── NEW.java
│ │ │ ├── PUT_FIELD.java
│ │ │ └── PUT_STATIC.java
│ │ ├── stack
│ │ ├── store
│ │ └── Factory
│ ├── rtda
│ │ ├── heap
│ │ │ ├── constantpool
│ │ │ ├── methodarea
│ │ │ │ ├── Class.java
│ │ │ │ ├── ClassMember.java
│ │ │ │ ├── Field.java
│ │ │ │ ├── Method.java
│ │ │ │ ├── MethodDescriptor.java
│ │ │ │ ├── MethodDescriptorParser.java
│ │ │ │ ├── MethodLookup.java
│ │ │ │ ├── Object.java
│ │ │ │ └── Slots.java
│ │ │ └── ClassLoader.java
│ │ ├── Frame.java
│ │ ├── JvmStack.java
│ │ ├── LocalVars.java
│ │ ├── OperandStack.java
│ │ ├── Slot.java
│ │ └── Thread.java
│ ├── Cmd.java
│ ├── Interpret.java
│ └── Main.java
└── test
└── java
└── org.ZYX.demo.test
└── HelloWorld.java
一、方法调用概述
从调用的角度来看,方法可以分为两类:静态方法(或者类方法)和实例方法。静态方法通过类来调用,实例方法则通过对象引用来调用。静态方法是静态绑定的,也就是说,最终调用的是哪个方法在编译期就已经确定。实例方法则支持动态绑定,最终要调用哪个方法可能要推迟到运行期才能知道,本章将详细讨论这一点。
从实现的角度来看,方法可以分为三类:没有实现(也就是抽象方法)、用Java语言(或者JVM上的其他语言,如Groovy和Scala等)实现和用本地语言(如C或者C++)实现。
静态方法和抽象方法是互斥的。接口只能包含抽象方法。为了实现Lambda表达式,Java 8放宽了这一限制,在接口中也可以定义静态方法和默认方法。本章不考虑接口的静态方法和默认方法。
本章只讨论Java方法的调用,本地方法调用将在第9章中介绍。
在Java 7之前,Java虚拟机规范一共提供了4条方法调用指令。其中invokestatic指令用来调用静态方法。invokespecial指令用来调用实例方法,包括构造函数、私有方法和父类中的方法。只要能被invokestatic和invokespecial调用的方法,都可以在解析阶段确定唯一的调用版本,包括静态方法、私有方法、实例构造器、父类方法。这些统称为“非虚方法”。
反之,则为“虚方法”。如果是针对接口类型的引用调用方法,就使用invokeinterface指令,否则使用invokevirtual指令。本章将实现这4条指令。
二、解析方法符号引用
非接口方法符号引用和接口方法符号引用的解析规则是不同的,因此本章分开讨论这两种符号引用。
1、非接口方法符号引用
打开MethodRef类文件,实现ResolvedMethod()方法,代码如下:
public Method ResolvedMethod() {
if (null == this.method) {
this.resolveMethodRef();
}
return this.method;
}
如果还没有解析过符号引用,调用resolveMethodRef()方法进行解析,否则直接返回方法指针。
resolveMethodRef()方法的代码如下:
private void resolveMethodRef() {
Class d = this.runTimeConstantPool.getClazz();
Class c = this.resolvedClass();
if (c.isInterface()) {
throw new IncompatibleClassChangeError();
}
Method method = lookupMethod(c, this.name, this.descriptor);
if (null == method){
throw new NoSuchMethodError();
}
if (!method.isAccessibleTo(d)){
throw new IllegalAccessError();
}
this.method = method;
}
如果类D想通过方法符号引用访问类C的某个方法,先要解析符号引用得到类C。如果C是接口,则抛出IncompatibleClassChangeError异常,否则根据方法名和描述符查找方法。如果找不到对应的方法,则抛出NoSuchMethodError异常,否则检查类D是否有权限访问该方法。如果没有,则抛出IllegalAccessError异常。isAccessibleTo()方法是在ClassMember类中定义的,在第6章就已经实现了。
下面看一下lookupMethod()方法,其代码如下:
public Method lookupMethod(Class clazz, String name, String descriptor) {
Method method = MethodLookup.lookupMethodInClass(clazz, name, descriptor);
if (null == method) {
method = MethodLookup.lookupMethodInInterfaces(clazz.interfaces, name, descriptor);
}
return method;
}
先从C的继承层次中找,如果找不到,就去C的接口中找。
LookupMethodInClass()和lookupMethodInInterfaces()方法在很多地方都要用到,所以在MethodLookup类文件中实现它,代码如下:
static public Method lookupMethodInClass(Class clazz, String name, String descriptor) {
for (Class c = clazz; c != null; c = c.superClass) {
for (Method method : c.methods) {
if (method.name.equals(name) && method.descriptor.equals(descriptor)) {
return method;
}
}
}
return null;
}
static public Method lookupMethodInInterfaces(Class[] ifaces, String name, String descriptor) {
for (Class inface : ifaces) {
for (Method method : inface.methods) {
if (method.name.equals(name) && method.descriptor.equals(descriptor)) {
return method;
}
}
}
return null;
}
非接口方法符号引用的解析就介绍完了。
2、接口方法符号引用
打开InterfaceMethodref类文件,在其中实现ResolvedInterfaceMethod()方法,代码如下:
public Method resolvedInterfaceMethod() {
if (this.method == null) {
this.resolveInterfaceMethodRef();
}
return this.method;
}
下面来看resolveInterface()方法,代码如下:
private void resolveInterfaceMethodRef() {
Class d = this.runTimeConstantPool.getClazz();
Class c = this.resolvedClass();
if (!c.isInterface()) {
throw new IncompatibleClassChangeError();
}
Method method = lookupInterfaceMethod(c, this.name, this.descriptor);
if (null == method) {
throw new NoSuchMethodError();
}
if (!method.isAccessibleTo(d)){
throw new IllegalAccessError();
}
this.method = method;
}
下面来看lookupInterfaceMethod()函数,代码如下:
private Method lookupInterfaceMethod(Class iface, String name, String descriptor) {
for (Method method : iface.methods) {
if (method.name.equals(name) && method.descriptor.equals(descriptor)) {
return method;
}
}
return MethodLookup.lookupMethodInInterfaces(iface.interfaces, name, descriptor);
}
如果能在接口中找到方法,就返回找到的方法,否则调用lookupMethodInInterfaces()函数在超接口中寻找。lookupMethodInInterfaces()函数已经在前面刚刚介绍。
三、方法调用和参数传递
在将符号引用解析成直接引用之后,就得到了需要调用的方法,Java虚拟机要给这个方法创建一个新的帧并把它推入Java虚拟机栈顶,然后传递参数。
这个逻辑对于本章要实现的4条方法调用指令来说基本上相同,为了避免重复代码,在单独的文件中实现这个逻辑。在instructions\base目录下创建MethodInvokeLogic类文件,在其中实现InvokeMethod()函数,代码如下:
public class MethodInvokeLogic {
public static void invokeMethod(Frame invokerFrame, Method method) {
//码创建新的帧并推入Java虚拟机栈;
Thread thread = invokerFrame.thread();
Frame newFrame = thread.newFrame(method);
thread.pushFrame(newFrame);
int argSlotCount = method.argSlotCount();
if (argSlotCount > 0) {
for (int i = argSlotCount - 1; i >= 0; i--) {
Slot slot = invokerFrame.operandStack().popSlot();
newFrame.localVars().setSlot(i, slot);
}
}
//hack
if (method.isNative()) {
if ("registerNatives".equals(method.name())) {
thread.popFrame();
} else {
throw new RuntimeException("native method " + method.name());
}
}
}
}
LocalVars类的setSlot()方法是新增的,代码如下:
public void setSlot(int idx, Slot slot) {
this.slots[idx] = slot;
}
再修改Method类,给它添加argSlotCount字段,代码如下:
public int maxStack;
public int maxLocals;
public byte[] code;
private int argSlotCount;
ArgSlotCount()只是个Getter方法而已,代码如下:
public int argSlotCount() {
return this.argSlotCount;
}
newMethods()方法也需要修改,在其中计算方法的argSlotCount,代码如下:
Method[] newMethods(Class clazz, MemberInfo[] cfMethods) {
Method[] methods = new Method[cfMethods.length];
for (int i = 0; i < cfMethods.length; i++) {
methods[i] = new Method();
methods[i].clazz = clazz;
methods[i].copyMemberInfo(cfMethods[i]);
methods[i].copyAttributes(cfMethods[i]);
methods[i].calcArgSlotCount();
}
return methods;
}
下面是calcArgSlotCount()方法的代码:
private void calcArgSlotCount() {
MethodDescriptor parsedDescriptor = MethodDescriptorParser.parseMethodDescriptorParser(this.descriptor);
List<String> parameterTypes = parsedDescriptor.parameterTypes;
for (String paramType : parameterTypes) {
this.argSlotCount++;
if ("J".equals(paramType) || "D".equals(paramType)) {
this.argSlotCount++;
}
}
if (!this.isStatic()) {
this.argSlotCount++;
}
}
parseMethodDescriptor()方法分解方法描述符,返回一个MethodDescriptor结构体实例。这个结构体定义在MethodDescriptor类文件中,代码如下:
public class MethodDescriptor {
public List<String> parameterTypes = new ArrayList<>();
public String returnType;
}
parseMethodDescriptor()函数定义在MethodDescriptorParser类文件中,为了节约篇幅,这里就不详细介绍这个方法了,感兴趣的读者请阅读源码。
四、返回指令
方法执行完毕之后,需要把结果返回给调用方,这一工作由返回指令完成。返回指令属于控制类指令,一共有6条。其中return指令用于没有返回值的情况,areturn、ireturn、lreturn、freturn和dreturn分别用于返回引用、int、long、float和double类型的值。
在instructions/control目录下创建rtn包,在其中定义6条指令。
6条返回指令都不需要操作数。RETURN指令比较简单,只要把当前帧从Java虚拟机栈中弹出即可,它的Execute()方法如下:
public class RETURN extends InstructionNoOperands {
@Override
public void execute(Frame frame) {
frame.thread().popFrame();
}
}
其他5条返回指令的Execute()方法都非常相似,为了节约篇幅,下面只给出IRETURN指令的代码:
public class IRETURN extends InstructionNoOperands {
@Override
public void execute(Frame frame) {
Thread thread = frame.thread();
Frame currentFrame = thread.popFrame();
Frame invokerFrame = thread.topFrame();
int val = currentFrame.operandStack().popInt();
invokerFrame.operandStack().pushInt(val);
}
}
Thread类的topFrame()方法和currentFrame()代码一样,这里用不同的名称主要是为了避免混淆。
方法符号引用解析、参数传递、结果返回等都实现了,下面实现方法调用指令。
五、方法调用指令
由于本书不考虑接口的静态方法和默认方法,所以要实现的这4条指令并没有完全满足Java虚拟机规范第8版的规定,下面从较简单的invokestatic指令开始。
①invokestatic指令:
我们直接来看代码:
public class INVOKE_STATIC extends InstructionIndex16 {
@Override
public void execute(Frame frame) {
RunTimeConstantPool runTimeConstantPool = frame.method().clazz().constantPool();
MethodRef methodRef = (MethodRef) runTimeConstantPool.getConstants(this.idx);
Method resolvedMethod = methodRef.ResolvedMethod();
if (!resolvedMethod.isStatic()) {
throw new IncompatibleClassChangeError();
}
MethodInvokeLogic.invokeMethod(frame, resolvedMethod);
}
}
假定解析符号引用后得到方法M。M必须是静态方法,否则抛出Incompatible-ClassChangeError异常。M不能是类初始化方法。类初始化方法只能由Java虚拟机调用,不能使用invokestatic指令调用。这一规则由class文件验证器保证,这里不做检查。如果声明M的类还没有被初始化,则要先初始化该类。将在第8小节讨论类的初始化。
对于invokestatic指令,M就是最终要执行的方法,调用InvokeMethod()函数执行该方法。
②invokespecial指令:
invokespecial指令代码如下,先看第一部分:
public void execute(Frame frame) {
Class currentClass = frame.method().clazz();
RunTimeConstantPool runTimeConstantPool = currentClass.constantPool();
MethodRef methodRef = (MethodRef) runTimeConstantPool.getConstants(this.idx);
Class resolvedClass = methodRef.resolvedClass();
Method resolvedMethod = methodRef.ResolvedMethod();
先拿到当前类、当前常量池、方法符号引用,然后解析符号引用,拿到解析后的类和方法。继续看代码:
if ("<init>".equals(resolvedMethod.name()) && resolvedMethod.clazz() != resolvedClass) {
throw new NoSuchMethodError();
}
if (resolvedMethod.isStatic()) {
throw new IncompatibleClassChangeError();
}
假定从方法符号引用中解析出来的类是C,方法是M。如果M是构造函数,则声明M的类必须是C,否则抛出NoSuchMethodError异常。如果M是静态方法,则抛出IncompatibleClassChangeError异常。继续看代码:
Object ref = frame.operandStack().getRefFromTop(resolvedMethod.argSlotCount() - 1);
if (null == ref) {
throw new NullPointerException();
}
从操作数栈中弹出this引用,如果该引用是null,抛出NullPointerException异常。GetRefFromTop()方法的代码很简单,在后面给出。继续看Execute()方法:
if (resolvedMethod.isProtected() &&
resolvedMethod.clazz().isSubClassOf(currentClass) &&
!resolvedMethod.clazz().getPackageName().equals(currentClass.getPackageName()) &&
ref.clazz() != currentClass &&
!ref.clazz().isSubClassOf(currentClass)) {
throw new IllegalAccessError();
}
上面的判断确保protected方法只能被声明该方法的类或子类调用。如果违反这一规定,则抛出IllegalAccessError异常。接着往下看:
Method methodToBeInvoked = resolvedMethod;
if (currentClass.isSuper() &&
resolvedClass.isSubClassOf(currentClass) &&
!resolvedMethod.name().equals("<init>")) {
MethodLookup.lookupMethodInClass(currentClass.superClass, methodRef.name(), methodRef.descriptor());
}
如果调用的中父类中的函数,但不是构造函数,且当前类的ACC_SUPER标志被设置,需要一个额外的过程查找最终要调用的方法;否则前面从方法符号引用中解析出来的方法就是要调用的方法。继续:
if (methodToBeInvoked.isAbstract()) {
throw new AbstractMethodError();
}
MethodInvokeLogic.invokeMethod(frame, methodToBeInvoked);
}
如果查找过程失败,或者找到的方法是抽象的,抛出AbstractMethodError异常。最后,如果一切正常,就调用方法。这里之所以这么复杂,是因为调用父类的(非构造函数)方法需要特别处理。
OperandStack类GetRefFromTop()方法的代码如下:
public Object getRefFromTop(int n) {
return this.slots[this.size - 1 - n].ref;
}
③invokevirtual指令:
invokevirtual指令代码如下,先看第一部分,和前面invokespecial差不多:
public void execute(Frame frame) {
Class currentClass = frame.method().clazz();
RunTimeConstantPool runTimeConstantPool = currentClass.constantPool();
MethodRef methodRef = (MethodRef) runTimeConstantPool.getConstants(this.idx);
Method resolvedMethod = methodRef.ResolvedMethod();
if (resolvedMethod.isStatic()) {
throw new IncompatibleClassChangeError();
}
Object ref = frame.operandStack().getRefFromTop(resolvedMethod.argSlotCount() - 1);
if (null == ref) {
if ("println".equals(methodRef.name())) {
_println(frame.operandStack(), methodRef.descriptor());
return;
}
throw new NullPointerException();
}
if (resolvedMethod.isProtected() &&
resolvedMethod.clazz().isSubClassOf(currentClass) &&
!resolvedMethod.clazz().getPackageName().equals(currentClass.getPackageName()) &&
ref.clazz() != currentClass &&
!ref.clazz().isSubClassOf(currentClass)) {
throw new IllegalAccessError();
}
然后继续:
Method methodToBeInvoked = MethodLookup.lookupMethodInClass(ref.clazz(), methodRef.name(), methodRef.descriptor());
if (null == methodToBeInvoked || methodToBeInvoked.isAbstract()) {
throw new AbstractMethodError();
}
MethodInvokeLogic.invokeMethod(frame, methodToBeInvoked);
}
从对象的类中查找真正要调用的方法。如果找不到方法,或者找到的是抽象方法,则需要抛出AbstractMethodError异常,否则一切正常,调用方法。
_println()函数如下:
private void _println(OperandStack stack, String descriptor) {
switch (descriptor) {
case "(Z)V":
System.out.println(stack.popInt() != 0);
break;
case "(C)V":
System.out.println(stack.popInt());
break;
case "(I)V":
case "(B)V":
case "(S)V":
System.out.println(stack.popInt());
break;
case "(F)V":
System.out.println(stack.popFloat());
break;
case "(J)V":
System.out.println(stack.popLong());
break;
case "(D)V":
System.out.println(stack.popDouble());
break;
default:
System.out.println(descriptor);
break;
}
stack.popRef();
}
④invokeinterface指令指令:
在instructions\references目录下创建invokeinterface类文件,在其中定义invokeinterface指令,代码如下:
public class INVOKE_INTERFACE implements Instruction {
private int idx; // count uint8; zero uint8;
@Override
public void fetchOperands(BytecodeReader reader) {
this.idx = reader.readShort();
reader.readByte(); //count;
reader.readByte(); //zero = 0;
}
}
在字节码中,invokeinterface指令的操作码后面跟着4字节而非2字节。前两字节的含义和其他指令相同,是个uint16运行时常量池索引。第3字节的值是给方法传递参数需要的slot数,其含义和给Method类定义的argSlotCount字段相同。第4字节是留给Oracle的某些Java虚拟机实现用的,它的值必须是0。该字节的存在是为了保证Java虚拟机可以向后兼容。
下面看Execute()方法,第一部分代码如下:
public void execute(Frame frame) {
RunTimeConstantPool runTimeConstantPool = frame.method().clazz().constantPool();
InterfaceMethodRef methodRef = (InterfaceMethodRef) runTimeConstantPool.getConstants(this.idx);
Method resolvedMethod = methodRef.resolvedInterfaceMethod();
if (resolvedMethod.isStatic() || resolvedMethod.isPrivate()) {
throw new IncompatibleClassChangeError();
}
先从运行时常量池中拿到并解析接口方法符号引用,如果解析后的方法是静态方法或私有方法,则抛出IncompatibleClassChangeError异常。继续看代码:
Object ref = frame.operandStack().getRefFromTop(resolvedMethod.argSlotCount() - 1);
if (null == ref) {
throw new NullPointerException();
}
if (!ref.clazz().isImplements(methodRef.resolvedClass())) {
throw new IncompatibleClassChangeError();
}
从操作数栈中弹出this引用,如果引用是null,则抛出NullPointerException异常。如果引用所指对象的类没有实现解析出来的接口,则抛出IncompatibleClassChangeError异常。继续看代码:
Method methodToBeInvoked = MethodLookup.lookupMethodInClass(ref.clazz(), methodRef.name(), methodRef.descriptor());
if (null == methodToBeInvoked || methodToBeInvoked.isAbstract()) {
throw new AbstractMethodError();
}
if (!methodToBeInvoked.isPublic()) {
throw new IllegalAccessError();
}
MethodInvokeLogic.invokeMethod(frame, methodToBeInvoked);
查找最终要调用的方法。如果找不到,或者找到的方法是抽象的,则抛出Abstract-MethodError异常。如果找到的方法不是public,则抛出IllegalAccessError异常,否则,一切正常,调用方法。
4条方法调用指令和6条返回指令都准备好了,还需要修改instructions\factory文件,在其中增加这些指令的case语句。
六、改进解释器
我们的解释器目前只能执行单个方法,现在就扩展它,让它支持方法调用。打开Interpreter类,修改interpret()方法,代码如下:
Interpret(Method method, boolean logInst) {
Thread thread = new Thread();
Frame frame = thread.newFrame(method);
thread.pushFrame(frame);
loop(thread, logInst);
}
logInst参数控制是否把指令执行信息打印到控制台。更重要的变化在loop()函数中,代码如下所示:
private void loop(Thread thread, boolean logInst) {
BytecodeReader reader = new BytecodeReader();
while (true) {
Frame frame = thread.currentFrame();
int pc = frame.nextPC();
thread.setPC(pc);
reader.reset(frame.method().code, pc);
byte opcode = reader.readByte();
Instruction inst = Factory.newInstruction(opcode);
if (null == inst) {
System.out.println("Unsupported opcode " + byteToHexString(new byte[]{opcode}));
break;
}
inst.fetchOperands(reader);
frame.setNextPC(reader.pc());
if (logInst) {
logInstruction(frame, inst, opcode);
}
//exec
inst.execute(frame);
if (thread.isStackEmpty()) {
break;
}
}
}
在每次循环开始,先拿到当前帧,然后根据pc从当前方法中解码出一条指令。指令执行完毕之后,判断Java虚拟机栈中是否还有帧。如果没有则退出循环;否则继续。Thread类的IsStackEmpty()方法是新增加的,代码如下所示:
public boolean isStackEmpty(){
return this.stack.isEmpty();
}
它只是调用了Stack类的isEmpty()方法,代码如下所示:
public boolean isEmpty(){
return this._top == null;
}
logInstruction()函数在方法执行过程中打印指令信息,代码如下:
private static void logInstruction(Frame frame, Instruction inst, byte opcode) {
Method method = frame.method();
String className = method.clazz().name();
String methodName = method.name();
String outStr = (className + "." + methodName + "() \t") +
"寄存器(指令):" + byteToHexString(new byte[]{opcode}) + " -> " + inst.getClass().getSimpleName() + " => 局部变量表:" + JSON.toJSONString(frame.localVars().getSlots()) + " 操作数栈:" + JSON.toJSONString(frame.operandStack().getSlots());
System.out.println(outStr);
}
解释器改造完毕,下面测试方法调用。
七、测试方法调用
先改造命令行工具,给它增加一个选项。java命令提供了-verbose:class(简写为-verbose)选项,可以控制是否把类加载信息输出到控制台。
parseCmd()函数也需要修改,改动比较简单,这里就不给出代码了。
然后修改Main类中的startJVM()函数,代码如下:
private static void startJVM(Cmd cmd) {
Classpath classpath = new Classpath(cmd.jre, cmd.classpath);
ClassLoader classLoader = new ClassLoader(classpath);
//获取className
String className = cmd.getMainClass().replace(".", "/");
Class mainClass = classLoader.loadClass(className);
Method mainMethod = mainClass.getMainMethod();
if (null == mainMethod) {
throw new RuntimeException("Main method not found in class " + cmd.getMainClass());
}
new Interpret(mainMethod, cmd.verboseClassFlag);
}
然后我们把HelloWorld类改成一个斐波那契数列,执行复杂计算:
public class HelloWorld {
public static void main(String[] args) {
long x = fibonacci(10);
System.out.println(x);
}
private static long fibonacci(long n) {
if (n <= 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
}
输出结果为55!
八、类初始化
第6章实现了一个简化版的类加载器,可以把类加载到方法区中。但是因为当时还没有实现方法调用,所以没有办法初始化类。现在可以把这个逻辑补上了。我们已经知道,类初始化就是执行类的初始化方法(clinit)。类的初始化在下列情况下触发:
1、执行new指令创建类实例,但类还没有被初始化。
2、执行putstatic、getstatic指令存取类的静态变量,但声明该字段的类还没有被初始化。
3、执行invokestatic调用类的静态方法,但声明该方法的类还没有被初始化。
4、当初始化一个类时,如果类的超类还没有被初始化,要先初始化类的超类。
5、执行某些反射操作时。
为了判断类是否已经初始化,需要给Class结构体添加一个字段:public boolean initStarted;
类的初始化其实分为几个阶段,但由于我们的类加载器还不够完善,所以先使用一个简单的布尔状态就足够了。initStarted字段表示类的clinit方法是否已经开始执行。
接下来给Class类添加两个方法,代码如下:
public boolean initStarted(){
return this.initStarted;
}
public void startInit(){
this.initStarted = true;
}
InitStarted()是Getter方法,返回initStarted字段值。StartInit()方法把initStarted字段设置成true。
然后对new指令、putstatic和getstatic指令、invokestatic指令进行修改,具体见源代码。
4条指令都修改完毕了,但是新增加的代码做了些什么?先判断类的初始化是否已经开始,如果还没有,则需要调用类的初始化方法,并终止指令执行。但是由于此时指令已经执行到了一半,也就是说当前帧的nextPC字段已经指向下一条指令,所以需要修改nextPC,让它重新指向当前指令。Frame类的revertNextPC()方法做了这样的操作,代码如下:
public void revertNextPC(){
this.nextPC = this.thread.pc();
}
nextPC调整好之后,下一步查找并调用类的初始化方法。这个逻辑是通用的,在instructions\base\classInitLogic类文件中实现它,代码如下:
public class ClassInitLogic {
public static void initClass(Thread thread, Class clazz) {
clazz.startInit();
scheduleClinit(thread, clazz);
initSuperClass(thread, clazz);
}
InitClass()函数先调用StartInit()方法把类的initStarted状态设置成true以免进入死环,然后调用scheduleClinit()函数准备执行类的初始化方法,代码如下:
private static void scheduleClinit(Thread thread, Class clazz) {
Method clinit = clazz.getClinitMethod();
if (null == clinit) return;
Frame newFrame = thread.newFrame(clinit);
thread.pushFrame(newFrame);
}
类初始化方法没有参数,所以不需要传递参数。Class类的getClinitMethod()方法如下:
public Method getClinitMethod(){
return this.getStaticMethod("<clinit>","()V");
}
注意,这里有意使用了scheduleClinit这个函数名而非invokeClinit,因为有可能要先执行超类的初始化方法,如函数initSuperClass()所示:
private static void initSuperClass(Thread thread, Class clazz) {
if (clazz.isInterface()) return;
Class superClass = clazz.superClass();
if (null != superClass && !superClass.initStarted()) {
initClass(thread, superClass);
}
}
如果超类的初始化还没有开始,就递归调用InitClass()函数执行超类的初始化方法,这样可以保证超类的初始化方法对应的帧在子类上面,使超类初始化方法先于子类执行。
类的初始化逻辑写完了,这里就不进行测试了。