Handwritten JVM Chapter 7 with Java -- method call and return

Chapter 4 implements the runtime data areas such as Java virtual machine stack and frame, which lays a foundation for the implementation of the method. Chapter 5 implements a simple interpreter and more than 150 instructions, which can execute a single method. Chapter 6 implements the method area, which clears the obstacles for method invocation. This chapter will implement method call and return. On this basis, we will also discuss the initialization of classes and objects.

Contents of this chapter

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

1, Method call overview

From the perspective of invocation, methods can be divided into two categories: static methods (or class methods) and instance methods. Static methods are called through classes, and instance methods are called through object references. Static methods are statically bound, that is, which method is finally called has been determined at compile time. The instance method supports dynamic binding, and the method to be called may not be known until the runtime, which will be discussed in detail in this chapter.

From the perspective of implementation, methods can be divided into three categories: no implementation (that is, abstract methods), implementation in Java language (or other languages on the JVM, such as Groovy and Scala), and implementation in local language (such as C or C + +).

Static methods and abstract methods are mutually exclusive. An interface can only contain abstract methods. In order to implement Lambda expressions, Java 8 eases this restriction, and static methods and default methods can also be defined in the interface. This chapter does not consider the static and default methods of the interface.

This chapter only discusses the call of Java methods, and the local method call will be introduced in Chapter 9.

Before Java 7, the Java virtual machine specification provided a total of four method call instructions. The invokestatic instruction is used to call static methods. The invokespecial instruction is used to call instance methods that do not need dynamic binding, including constructors, private methods and superclass methods called through the super keyword. The rest belongs to dynamic binding. If the method is called against the reference of the interface type, the invokeinterface instruction is used; otherwise, the invokevirtual instruction is used. This chapter will implement these four instructions.

2, Parsing method symbol reference

The resolution rules of non interface method symbol reference and interface method symbol reference are different, so these two symbol references are discussed separately in this chapter.

1. Non interface method symbol reference

Open the MethodRef class file and implement the ResolvedMethod() method. The code is as follows:

   public Method ResolvedMethod() {
        if (null == this.method) {
            this.resolveMethodRef();
        }
        return this.method;
    }

If the symbol reference has not been resolved, call the resolveMethodRef () method to resolve it. Otherwise, return the method pointer directly.

The code of resolveMethodRef() method is as follows:

   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;
    }

If class D wants to access a method of class C through method symbolic reference, it must first resolve the symbolic reference to get class C. If C is an interface, an IncompatibleClassChangeError exception will be thrown. Otherwise, find the method according to the method name and descriptor. If the corresponding method cannot be found, a NoSuchMethodError exception will be thrown. Otherwise, check whether class D has permission to access the method. If not, an IllegalAccessError exception is thrown. isAccessibleTo () method is defined in ClassMember class and has been implemented in Chapter 6.

Let's take a look at the lookupMethod() method. Its code is as follows:

  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;
    }

First find it from the inheritance level of C. If you can't find it, go to the interface of C.

LookupMethodInClass() and lookupmethodyinterfaces() methods are used in many places, so they are implemented in the MethodLookup class file. The code is as follows:

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;
    }

The resolution of non interface method symbol reference is introduced.

2. Interface method symbol reference

Open InterfaceMethodref class file and implement ResolvedInterfaceMethod() method in it. The code is as follows:

 public Method resolvedInterfaceMethod() {
        if (this.method == null) {
            this.resolveInterfaceMethodRef();
        }
        return this.method;
    }

Let's look at the resolveInterface () method. The code is as follows:

   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;
    }

Let's look at the lookupInterfaceMethod() function. The code is as follows:

   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);
    }

If the method can be found in the interface, it will return the found method. Otherwise, call the lookupmethodyinterfaces() function to find it in the super interface. The lookupmethodyinterfaces() function has been described earlier.

3, Method invocation and parameter passing

After parsing the symbolic reference into a direct reference, you get the method to be called. The Java virtual machine needs to create a new frame for this method, push it to the top of the Java virtual machine stack, and then pass the parameters.

This logic is basically the same for the four method call instructions to be implemented in this chapter. In order to avoid repeated code, this logic is implemented in a separate file. Create a MethodInvokeLogic class file in the instructions\base directory, and implement the InvokeMethod() function in it. The code is as follows:

public class MethodInvokeLogic {

    public static void invokeMethod(Frame invokerFrame, Method method) {

        //Code to create a new frame and push it into the Java virtual machine stack;
        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());
            }
        }
    }

}

The setSlot() method of LocalVars class is newly added. The code is as follows:

    public void setSlot(int idx, Slot slot) {
        this.slots[idx] = slot;
    }

Then modify the Method class and add the argSlotCount field to it. The code is as follows:

    public int maxStack;
    public int maxLocals;
    public byte[] code;
    private int argSlotCount;

ArgSlotCount() is just a Getter method. The code is as follows:

    public int argSlotCount() {
        return this.argSlotCount;
    }

The newMethods () method also needs to be modified, in which the argSlotCount of the method is calculated, and the code is as follows:

  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;
    }

The following is the code of the calcArgSlotCount() method:

  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++;
        }
    }

The parseMethodDescriptor () method decomposes the method descriptor and returns an instance of the MethodDescriptor structure. This structure is defined in the MethodDescriptor class file. The code is as follows:

public class MethodDescriptor {

    public List<String> parameterTypes = new ArrayList<>();
    public String returnType;
}

The parseMethodDescriptor () function is defined in the MethodDescriptorParser class file. In order to save space, this method will not be introduced in detail here. Interested readers please read the source code.

4, Return instruction

After the method is executed, the result needs to be returned to the caller, which is completed by the return instruction. There are 6 return instructions belonging to the control class. The return instruction is used when there is no return value. areturn, ireturn, lreturn, freturn and dreturn are used to return values of reference, int, long, float and double types respectively.

Create the rtn package in the instructions/control directory and define 6 instructions in it.

None of the six RETURN instructions require operands. The RETURN instruction is relatively simple. Just pop up the current frame from the Java virtual machine stack. Its Execute() method is as follows:

public class RETURN extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        frame.thread().popFrame();
    }
    
}

The Execute() method of the other five return instructions is very similar. In order to save space, only the code of IRETURN instruction is given below:

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);
    }

}

The topFrame () method of Thread class is the same as the currentFrame () code. Different names are used here to avoid confusion.

Method symbol reference resolution, parameter transfer, result return, etc. are all implemented. The method call instruction is implemented below.

5, Method call instruction

Since this book does not consider the static method and default method of the interface, the four instructions to be implemented do not fully meet the provisions of the 8th edition of the Java virtual machine specification. Let's start with the simpler invokestatic instruction.

① invokestatic instruction:

Let's look directly at the code:

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);
    }
}

Assume that method m is obtained after parsing the symbolic reference. M must be a static method, or an incompatible classchangeerror exception will be thrown. M cannot be a class initialization method. Class initialization method can only be called by Java virtual machine, and cannot be called by invokestatic instruction. This rule is not guaranteed by the class verifier. If the class declaring m has not been initialized, it must be initialized first. Class initialization is discussed in Section 8.

For the invokestatic instruction, M is the final method to be executed. Call the InvokeMethod() function to execute the method.

② invokespecial instruction:

The invokespecial instruction code is as follows. Let's look at the first part first:

 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();

First get the current class, current constant pool and method symbol references, and then parse the symbol references to get the parsed classes and methods. Continue to look at the code:

 if ("<init>".equals(resolvedMethod.name()) && resolvedMethod.clazz() != resolvedClass) {
            throw new NoSuchMethodError();
        }
 if (resolvedMethod.isStatic()) {
            throw new IncompatibleClassChangeError();
        }

Suppose that the class resolved from the method symbol reference is C and the method is m. If M is a constructor, the class declaring m must be C, otherwise NoSuchMethodError exception is thrown. If M is a static method, an IncompatibleClassChangeError exception is thrown. Continue to look at the code:

 Object ref = frame.operandStack().getRefFromTop(resolvedMethod.argSlotCount() - 1);
 if (null == ref) {
     throw new NullPointerException();
        }

Pop up this reference from the operand stack. If the reference is null, throw a NullPointerException exception. The code of GetRefFromTop () method is very simple, which will be given later. Continue to look at the Execute() method:

 if (resolvedMethod.isProtected() &&
                resolvedMethod.clazz().isSubClassOf(currentClass) &&
                !resolvedMethod.clazz().getPackageName().equals(currentClass.getPackageName()) &&
                ref.clazz() != currentClass &&
                !ref.clazz().isSubClassOf(currentClass)) {
            throw new IllegalAccessError();
        }

The above judgment ensures that the protected method can only be called by the class or subclass that declares the method. If this rule is violated, an IllegalAccessError exception is thrown. Then look down:

      Method methodToBeInvoked = resolvedMethod;
      if (currentClass.isSuper() &&
                resolvedClass.isSubClassOf(currentClass) &&
                !resolvedMethod.name().equals("<init>")) {
            MethodLookup.lookupMethodInClass(currentClass.superClass, methodRef.name(), methodRef.descriptor());
        }

If the function in the parent class is called, but it is not a constructor, and the ACC of the current class_ When the super flag is set, an additional process is needed to find the method to be called finally; Otherwise, the method previously resolved from the method symbol reference is the method to be called. continue:

     if (methodToBeInvoked.isAbstract()) {
            throw new AbstractMethodError();
        }

        MethodInvokeLogic.invokeMethod(frame, methodToBeInvoked);

    }

If the search process fails or the method found is abstract, an AbstractMethodError exception is thrown. Finally, if everything is OK, call the method. The reason why this is so complicated is that calling the (non constructor) method of the parent class requires special handling.

The code of GetRefFromTop() method of OperandStack class is as follows:

    public Object getRefFromTop(int n) {
        return this.slots[this.size - 1 - n].ref;
    }

③ invokevirtual Directive:

The invokevirtual instruction code is as follows. First, look at the first part, which is similar to the previous 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();
        }

Then continue:

       Method methodToBeInvoked = MethodLookup.lookupMethodInClass(ref.clazz(), methodRef.name(), methodRef.descriptor());
       if (null == methodToBeInvoked || methodToBeInvoked.isAbstract()) {
            throw new AbstractMethodError();
       }

       MethodInvokeLogic.invokeMethod(frame, methodToBeInvoked);
    }

Find the method you really want to call from the class of the object. If a method cannot be found or an abstract method is found, you need to throw an AbstractMethodError exception. Otherwise, everything is normal and the method is called.

_ The println() function is as follows:

 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 instruction:

Create the invokeinterface class file in the instructions\references directory and define the invokeinterface instruction in it. The code is as follows:

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;
    }
}    

In bytecode, the opcode of the invokeinterface instruction is followed by 4 bytes instead of 2 bytes. The first two bytes have the same meaning as other instructions. They are a uint16 runtime constant pool index. The value of the third byte is the number of slots required to pass parameters to the Method, which has the same meaning as the argSlotCount field defined for the Method class. The 4th byte is reserved for some Java virtual machine implementations of Oracle, and its value must be 0. This byte exists to ensure backward compatibility of the Java virtual machine.

Let's look at the Execute() method. The first part of the code is as follows:

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();
        }

First get and resolve the symbolic reference of the interface method from the runtime constant pool. If the resolved method is a static method or a private method, an IncompatibleClassChangeError exception will be thrown. Continue to look at the code:

Object ref = frame.operandStack().getRefFromTop(resolvedMethod.argSlotCount() - 1);
        if (null == ref) {
            throw new NullPointerException();
        }
        if (!ref.clazz().isImplements(methodRef.resolvedClass())) {
            throw new IncompatibleClassChangeError();
        }

Pop up this reference from the operand stack. If the reference is null, a NullPointerException exception will be thrown. If the class that refers to the object does not implement the resolved interface, an IncompatibleClassChangeError exception is thrown. Continue to look at the code:

 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);

Find the final method to call. If it cannot be found or the method found is abstract, an abstract methoderror exception is thrown. If the found method is not public, an IllegalAccessError exception will be thrown. Otherwise, everything is normal and the method will be called.

Four method call instructions and six return instructions are ready. You also need to modify the instructions\factory file and add the case statements of these instructions.

6, Improved interpreter

Our Interpreter can only execute a single method at present. Expand it now to support method calls. Open the Interpreter class and modify the interpret() method. The code is as follows:

  Interpret(Method method, boolean logInst) {
        Thread thread = new Thread();
        Frame frame = thread.newFrame(method);
        thread.pushFrame(frame);

        loop(thread, logInst);
    }

The logInst parameter controls whether to print the instruction execution information to the console. The more important change is in the loop() function. The code is as follows:

 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;
            }
        }
    }

At the beginning of each cycle, first get the current frame, and then decode an instruction from the current method according to the pc. After the instruction is executed, judge whether there are frames in the Java virtual machine stack. If not, exit the cycle; Otherwise, continue. The IsStackEmpty() method of Thread class is newly added. The code is as follows:

 public boolean isStackEmpty(){
        return this.stack.isEmpty();
    }

It just calls isEmpty() method of Stack class, and the code is as follows:

public boolean isEmpty(){
        return this._top == null;
    }

The logInstruction() function prints instruction information during method execution. The code is as follows:

 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") +
                "register(instructions): " + byteToHexString(new byte[]{opcode}) + " -> " + inst.getClass().getSimpleName() + " => Local variable table:" + JSON.toJSONString(frame.localVars().getSlots()) + " Operand stack:" + JSON.toJSONString(frame.operandStack().getSlots());
        System.out.println(outStr);
    }

After the transformation of the interpreter, the following test method is called.

7, Test method call

First modify the command line tool and add an option to it. The java command provides the - verbose: class (abbreviated as - verbose) option to control whether to output the class loading information to the console.

The parseCmd() function also needs to be modified. The change is relatively simple. The code is not given here.

Then modify the startJVM() function in the Main class. The code is as follows:

 private static void startJVM(Cmd cmd) {
        Classpath classpath = new Classpath(cmd.jre, cmd.classpath);
        ClassLoader classLoader = new ClassLoader(classpath);
        //Get 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);
    }

Then we change the HelloWorld class into a Fibonacci sequence to perform complex calculations:

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);
        }
    }

}

The output is 55!

8, Class initialization

Chapter 6 implements a simplified class loader, which can load classes into the method area. However, because the method call was not implemented at that time, there was no way to initialize the class. Now you can make up this logic. As we already know, class initialization is to execute the class's initialization method (clinit). Class initialization is triggered when:
1. Execute the new instruction to create a class instance, but the class has not been initialized.
2. Execute the putstatic and getstatic instructions to access the static variables of the class, but the class that declares the field has not been initialized.
3. invokestatic executes the static method of the calling class, but the class that declares the method has not been initialized.
4. When initializing a class, if the superclass of the class has not been initialized, the superclass of the class must be initialized first.
5. When performing some reflection operations.

To determine whether a Class has been initialized, you need to add a field to the Class structure: public boolean initStarted;

Class initialization is actually divided into several stages, but because our class loader is not perfect enough, it is enough to use a simple Boolean state first. The initStarted field indicates whether the clinit method of the class has started execution.

Next, add two methods to Class. The code is as follows:

  public boolean initStarted(){
        return this.initStarted;
    }

  public void startInit(){
        this.initStarted = true;
    }

Initstarted() is a Getter method that returns the value of the initstarted field. The StartInit() method sets the initstarted field to true.

Then modify the new instruction, putstatic and getstatic instructions and invokestatic instructions. See the source code for details.

All four instructions have been modified, but what has the newly added code done? First judge whether the initialization of the class has started. If not, you need to call the initialization method of the class and terminate the instruction execution. However, since the instruction has been half executed at this time, that is, the nextPC field of the current Frame has pointed to the next instruction, it is necessary to modify nextPC to point to the current instruction again. The revertNextPC() method of Frame class does the following operation:

public void revertNextPC(){
        this.nextPC = this.thread.pc();
    }

After nextPC is adjusted, the next step is to find and call the initialization method of the class. This logic is universal. It is implemented in the instructions\base\classInitLogic class file. The code is as follows:

public class ClassInitLogic {

    public static void initClass(Thread thread, Class clazz) {
        clazz.startInit();
        scheduleClinit(thread, clazz);
        initSuperClass(thread, clazz);
    }

The InitClass () function first calls the StartInit () method to set the initStarted state of the class to true to avoid entering the dead ring, and then calls the scheduleClinit () function to prepare the initialization method for the class. The code is as follows:

  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 initialization method has no parameters, so there is no need to pass parameters. The getClinitMethod () method of class is as follows:

 public Method getClinitMethod(){
        return this.getStaticMethod("<clinit>","()V");
    }

Note that the function name scheduleClinit is intentionally used here instead of invokeClinit, because it is possible to execute the initialization method of the superclass first, as shown in the function 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);
        }
    }

If the initialization of the superclass has not started yet, recursively call the InitClass() function to execute the initialization method of the superclass, which can ensure that the frame corresponding to the initialization method of the superclass is above the subclass, so that the initialization method of the superclass is executed before the subclass.

After the initialization logic of the class is written, there will be no test here.

Keywords: Java jvm

Added by whir on Wed, 02 Feb 2022 10:16:29 +0200