用Java手写JVM第七章——方法调用和返回

第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异常。最后,如果一切正常,就调用方法。这里之所以这么复杂,是因为调用父类的(非构造函数)方法需要特别处理。

OperandStackGetRefFromTop()方法的代码如下:

    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()函数执行超类的初始化方法,这样可以保证超类的初始化方法对应的帧在子类上面,使超类初始化方法先于子类执行。

类的初始化逻辑写完了,这里就不进行测试了。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值