A Java Implementation of a Super Simple JVM

A slightly more sophisticated JVM implementation is described in the JVM Project.

The only data type for the Super Simple JVM (SSJVM) is int. There are no objects, floats, etc. Also, there is no heap and no class area (although these are easy enough to add).

Design

Note that all fields and methods have package scope (that's what "~" means) so you won't need to bother with constructors, setters, or getters.

An instruction has an optional label, a required operator, and 0, 1, or 2 operands. For example:

iadd                          ; 0 operands

istore 3                      ; 1 operand

getfield Account/balance D    ; 2 operands

See Jasmin Instructions for a list of (most of the) instructions.

A method (JMethod) has an array capable of storing up to 100 instructions. By default we assume a method can have up to 10 parameters and locals.

Stack frames can be linked together through the cf (calling frame) links. We think of this linked list as the Java stack. Note that the JVM contains a pointer (topFrame) to the top-most frame on this tack (i.e., the frame at the head of the list).

A stack frame is holds the locals, parameters, and temporary values for an executing method (meth). The pc (program counter) is the index of the next instruction in meth.instructions to be executed, i.e., meth.instructions[pc].

Locals is an array of up to 10 ints. These are the parameters and local variables needed by meth.

Operands is a stack if integers (note that Integer and int are interchangeable in most situations) where intermediate values are stored for lengthy computations.

Implementation

A partial implementation can be found in TestJVM.java.

JVM

Here's a partial implementation of the JVM:

class JVM {
   StackFrame topFrame;
   // ClassArea classes;  not used
   // Heap heap;  not used
   boolean DEBUG = true;

   // fetch-execute loop:
   public void run() {
      while(topFrame != null) {
         Instruction next = topFrame.getNextInstruction();
         if (next == null) break;
         execute(next);
         if (DEBUG) System.out.println(topFrame);
      }
   }

   // dispatcher:
   void execute(Instruction inst) {
      String op = inst.operator;
      if (op.equals("iload")) {
         executeIload(inst);
      } else if (op.equals("istore")) {
         executeIstore(inst);
      } else if (op.equals("ifgt")) {
         executeIfgt(inst);
      } else if (op.equals("iadd")) {
         executeIadd(inst);
      }  else if (op.equals("ldc")) {
         executeLdc(inst);
      } // etc.
      else {
         System.out.println("unrecognized operator: " + op);
         System.exit(1);
      }
   }

   // specialized handlers:
   void executeLdc(Instruction inst) { }
   void executeIadd(Instruction inst) { }
   void executeIload(Instruction inst) { }
   void executeIstore(Instruction inst) { }
   void executeIfgt(Instruction inst) { }
   // etc.
}

The run method is essentially the fetch-execute cycle for the JVM. It fetches the next instruction to execute from the top frame, then passes it off to the execute method.

The execute method is an example of a dispatcher. Based on the operator of the instruction, control is dispatched to a low level handler method that specializes in executing just one type of instruction.

For example, here is how the handlers for the iadd and ldc instructions are implemented:

// inst = ldc n
void executeLdc(Instruction inst) {
   Integer n = new Integer(inst.operand1); // convert operand1 (= n) to an Integer
   topFrame.operands.push(n);
}

// inst = iadd
void executeIadd(Instruction inst) {
   if (topFrame.operands.size() < 2) {
      System.out.println("stack too small for iadd");
      System.exit(1);
   }
   Integer a = topFrame.operands.pop();
   Integer b = topFrame.operands.pop();
   Integer sum = a + b;
   topFrame.operands.push(sum);
}

TestJVM

Here's a simple test driver for JVM:

public class TestJVM {
  public static void main(String[] args) {

     // create a JVM with a top frame with a method:
     JMethod aMeth = new JMethod();
     StackFrame frame = new StackFrame();
     frame.meth = aMeth;
     JVM jvm = new JVM();
     jvm.topFrame = frame;

     // load method with some instructions:


     // ldc 10
     Instruction a = new Instruction();
     a.operator = "ldc";
     a.operand1 = "10";
     aMeth.instructions[0] = a;

     // ldc 32
     a = new Instruction();
     a.operator = "ldc";
     a.operand1 = "32";
     aMeth.instructions[1] = a;

     // iadd
     a = new Instruction();
     a.operator = "iadd";
     aMeth.instructions[2] = a;

     // and now run the jvm:
     jvm.run();
  }
}

Here's the output produced:

operands: [10]
locals = [0000000000]
operands: [10, 32]
locals = [0000000000]
operands: [42]
locals = [0000000000]

Note that 42 = 10 + 32.

The Lab

Complete and test the following handlers:

void executeLoad(Instruction inst) { }
void executeStore(Instruction inst) { }
void executeIfgt(Instruction inst) { }

Add the ability to execute goto and imul.