What it is
IJVM is a cut-down stack machine — an integer-only teaching subset of the JVM, defined in Tanenbaum's Structured Computer Organization. It has no registers in the conventional sense: every operation takes its arguments off an operand stack and pushes its result back.
Writing an emulator for it means building the whole machine in software. You parse a binary file into memory regions, then run a loop that reads the byte at the program counter, decides what it means, and does it — for every instruction, forever, until the program halts or does something illegal.
Memory model
The bulk of the work is not arithmetic, it is memory discipline. The binary is parsed into separate regions that must not be allowed to bleed into each other:
- Constant pool — read-only data loaded from the binary, addressed by index rather than by pointer.
- Method area — raw bytecode, which the fetch loop walks over but never treats as data.
- Operand stack — grows and shrinks with every instruction; needs manual reallocation in C when it outgrows its initial allocation.
- Local variable frame — method parameters and locals, addressed relative to a frame pointer rather than absolutely, so the same bytecode works wherever the frame happens to live.
In C none of this is enforced for you. A stack that grows past its allocation does not raise an exception — it quietly writes over whatever is next in memory, and the program fails somewhere unrelated, several thousand instructions later.
The fetch–decode–execute loop
The core is a dispatch loop: read the opcode at the program counter, decode its operands, execute, advance. Most instructions are straightforward once the memory model is right — arithmetic pops two values and pushes one, branches adjust the program counter, loads and stores move between the frame and the stack.
Two things complicate it. The WIDE prefix modifies the instruction that
follows it to take a 16-bit index instead of 8-bit, so decoding is not a flat switch on
one byte — it has state. And operand widths vary, so every read has to know how many
bytes it is consuming and advance the counter correspondingly.
Method calls
INVOKEVIRTUAL and IRETURN are where the design is actually
tested. A call has to:
- Read the method's argument count and local variable count from its header in the constant pool.
- Build a new frame, with the caller's arguments already sitting on the stack becoming the callee's first locals.
- Save the caller's context — program counter, frame pointer, stack pointer — somewhere the callee cannot reach.
- On return, pop the return value, restore all three saved pointers, and push the value into the caller's stack as if nothing had happened.
Get any part of that wrong and recursion is what exposes it: a single-level call appears to work while a nested one silently corrupts the frame below it. Recursive test programs were the real correctness check.
Why it was worth doing
Every abstraction above this level — a stack frame, a local variable, a function call — stops being a language feature and becomes something you have explicitly built out of pointer arithmetic. It is the clearest possible demonstration of what the compiler and runtime are doing on your behalf the rest of the time.