skip to content
everything.jvm.bytecode

bytecode

a stack machine. watch the operand stack while a method runs.

chapter
chapter 13.03
reading time
8 min read

the JVM is a stack machine. every method call gets a frame with two things in it: a local variable array (numbered slots for this, the parameters and your locals) and an operand stack. instructions push values onto the stack, pop them off, and push results back. there are no registers.

a + b is "push a, push b, add". that's bytecode.

the picture

you know how a calculator with an "enter" key (the old HP kind) never needs parentheses? you type 3 enter 4 + and it works, because every number goes on a stack and every operation eats the top of it. that's reverse polish notation, and it's exactly how the JVM thinks.

the local variable array is the calculator's memory buttons (named slots you can store into and recall from). the operand stack is the display stack (where the actual math happens). every instruction is one button press.

so int c = a + b; becomes four presses: recall a, recall b, add, store into c.

watch it run

pick a method, then press step. one step is one instruction. the bytecode is real: it's what javap -c prints for these methods on jdk 25.

method =
Calc.java
  1. 1static int add(int a, int b) {
  2. 2 int c = a + b;
  3. 3 return c;
  4. 4}
static int add(int, int) just ran
stack=2, locals=3
  1. 0:iload_0
  2. 1:iload_1
  3. 2:iadd
  4. 3:istore_2
  5. 4:iload_2
  6. 5:ireturn
frames · newest on top
addnext pc 0
local variables
0a3
1b4
2c·
operand stack
max_stack 2

add(3, 4) is called. the JVM makes a frame: a local variable array with a in slot 0 and b in slot 1, and an empty operand stack. c has a slot reserved, but nothing in it yet.

instruction 1/7
the hot bar is the instruction that just ran; the outlined row is the next one. watch the operand stack: values only ever go on or come off the top. in a method call, see how the first result waits on the caller's stack while square runs again. in new Point(1, 2), the object exists with zeroed fields four instructions before its constructor starts, and the blue values are references.

three things to notice:

  1. locals are numbered, not named. a is slot 0, b is slot 1. in an instance method slot 0 is always this (step into Point.<init> to see it). the names only survive in an optional debug table (javac -g), which is how your debugger shows them.
  2. the stack has a fixed max depth. javac computes it per method (stack=2 above) and writes it into the class file, so the JVM can allocate the frame up front. new Point(1, 2) needs 4: two references and two ints.
  3. calls pass values by copying them. invokestatic pops the arguments off the caller's stack and they become the callee's first locals. that's pass-by-value, at the instruction level.

reading javap

here's the add method, as javap -c -p prints it:

javap -c -p Calc
static int add(int, int);
  Code:
     0: iload_0
     1: iload_1
     2: iadd
     3: istore_2
     4: iload_2
     5: ireturn

the number on the left is the byte offset of the instruction inside the method (the pc). most instructions are one byte, so they count up by one; if_icmpge 19 is three (an opcode plus a two-byte jump offset), which is why the loop above goes 6, 9, 10.

the first letter of an instruction is its type. the JVM never figures out types at runtime: javac picked the instruction for you, and the verifier checked it before anything ran.

prefixtypee.g.
iint (and boolean, byte, char, short)iload, iadd, ireturn
llonglload, lmul, lreturn
ffloatfadd, fcmpl
ddoubledadd, dcmpg
areferenceaload, areturn, aastore
b c sbyte, char, short arraysbaload, castore, saload

and the families, roughly 200 opcodes in all (an opcode is one byte, so there's room for 256):

  • ├── locals ↔ operand stack
  • │ └── iload_0 aload_1 istore_2 ...
  • ├── push a literal
  • │ └── iconst_0..5 bipush sipush ldc aconst_null
  • ├──
  • │ └── iadd lmul ddiv irem ineg ishl i2l d2i ...
  • ├── all control flow is this
  • │ └── if_icmpge ifeq ifnull goto tableswitch lookupswitch
  • ├──
  • │ └── new getfield putfield getstatic newarray arraylength checkcast instanceof
  • ├──
  • │ └── dup dup_x1 pop swap
  • ├── the next chapter
  • │ └── invokestatic invokevirtual invokeinterface invokespecial invokedynamic
  • ├──
  • │ └── ireturn areturn return
  • └──
  • └── athrow monitorenter monitorexit
quick check
x and y are doubles. what does x + y compile to?

why a stack machine?

real CPUs have registers. why didn't the JVM?

  • portability. a stack machine assumes nothing about the CPU underneath. x86 has 16 general-purpose registers, ARM64 has 31. bytecode doesn't care.
  • size. iload_0 doesn't need to name a register, so it fits in one byte. java was designed in the 90s to ship code over slow networks.
  • verification. with a fixed stack depth and typed instructions, the JVM can check a method in one pass before running it: no stack underflow, no int used as a reference.

the catch: stack code is slow to interpret literally. but nothing runs it literally for long. the JIT turns hot methods into register-based machine code, and the operand stack mostly disappears. Android's runtime went the other way and used a register-based bytecode (dex) for its interpreter.

predict the output
java
int i = 5;
i = i++;
IO.println(i);
Main.java
void main() {
    int i = 5;
    i = i++;
    IO.println(i);
}
output
5

long and double take two slots

in the spec's model, one local slot (or one operand stack entry) holds an int, a float or a reference. a long or a double takes two of each.

javap -v Misc (static long twice(long x) { return x * 2; })
static long twice(long);
  stack=4, locals=2, args_size=1
     0: lload_0
     1: ldc2_w  #7      // long 2l
     4: lmul
     5: lreturn
local variable array
x
0
x
1
operand stack at lmul
x
0
x
1
2L
2
2L
3
one parameter, two local slots. two values on the stack, four entries. this is why a long after an int parameter lands in slots 1 and 2, and the next parameter starts at 3.
one level deeper:tableswitch vs lookupswitch+

a switch on ints compiles to one of two instructions. with dense keys (1, 2, 3) javac emits tableswitch: a jump table indexed by key - low, so finding the case is one array lookup. with sparse keys (1 and 1000) it emits lookupswitch: a sorted list of key/offset pairs the JVM can binary-search. javac picks by comparing the estimated cost of both. switch on strings compiles to a lookupswitch on hashCode() followed by equals checks, and pattern switch uses invokedynamic (SwitchBootstraps.typeSwitch).

keylines
  • a frame is a local variable array plus an operand stack. that's the whole machine.
  • instructions push and pop. a + b is iload, iload, iadd.
  • the type is in the opcode: iadd, ladd, dadd. nothing is checked at runtime.
  • loops are jumps. new is allocate, dup, invokespecial. calls copy arguments into the callee's locals.
  • javap -c -p shows you all of it. read it whenever java surprises you.