Bytecode Basics in Java
Bytecode is the instruction set of the Java Virtual Machine: the compact, platform-independent code that javac writes into .class files and the JVM executes. Each instruction is one byte (the opcode, which gives bytecode its name), sometimes followed by a few bytes of operands. The JVM is a stack machine: instead of working with CPU registers, instructions push values onto an operand stack and pop them off. In this lesson, you will learn how a method's frame is organized, the main families of instructions, how a loop looks in bytecode, and the five instructions that call methods: invokestatic, invokevirtual, invokeinterface, invokespecial and invokedynamic.
You don't write bytecode by hand, but reading it explains how Java features really work and what the JIT compiler starts from. "Javap Tool in Java" showed how to print it.
The Frame: Local Variables and the Operand Stack
Every method call gets a frame on the thread's stack (see "Stack vs Heap Memory in Java"). A frame has two working areas:
| Area | What it holds |
|---|---|
| Local variable array | Numbered slots for this (slot 0 in instance methods), then the parameters, then local variables. long and double take two slots. |
| Operand stack | A working stack. Instructions pop their inputs from it and push their results onto it. |
The compiler records the sizes both need (stack=2, locals=3 in javap -v), so the JVM can allocate the frame in one step.
Instruction Families
Most instruction names begin with a letter for the type they work on:
| Prefix | Type |
|---|---|
i | int (also used for boolean, byte, char and short) |
l | long |
f | float |
d | double |
a | A reference (an object or array) |
| Family | Examples | What they do |
|---|---|---|
| Constants | iconst_0 ... iconst_5, bipush 35, sipush 1000, ldc #7, aconst_null | Push a constant |
| Load | iload_1, aload_0, iload 7 | Push a local variable onto the stack |
| Store | istore_1, astore_2 | Pop the stack top into a local variable |
| Arithmetic | iadd, isub, imul, idiv, irem, ineg, iinc 2, 1 | Pop operands, push the result (iinc changes a local directly) |
| Conversion | i2l, i2d, l2d, d2i, i2b | Convert between primitive types |
| Objects | new, getfield, putfield, getstatic, putstatic, checkcast, instanceof | Create objects, access fields, cast |
| Arrays | newarray, arraylength, iaload, iastore | Create and access arrays |
| Stack | dup, pop, swap | Rearrange the stack |
| Control | if_icmpgt, ifeq, ifnull, goto, tableswitch, lookupswitch | Jump to another position |
| Calls | invokestatic, invokevirtual, invokeinterface, invokespecial, invokedynamic | Call methods |
| Return | ireturn, areturn, return | Return a value, or nothing |
Short forms such as iload_1 and iconst_3 exist for the most common cases, so they take a single byte.
Example: A Loop in Bytecode
public class Sum {
static int sumTo(int n) {
int total = 0;
for (int i = 1; i <= n; i++) {
total += i;
}
return total;
}
public static void main(String[] args) {
System.out.println(sumTo(4));
}
}10 static int sumTo(int);
Code:
0: iconst_0
1: istore_1
2: iconst_1
3: istore_2
4: iload_2
5: iload_0
6: if_icmpgt 19
9: iload_1
10: iload_2
11: iadd
12: istore_1
13: iinc 2, 1
16: goto 4
19: iload_1
20: ireturnExplanation: The method is static, so slot 0 is n, slot 1 is total and slot 2 is i.
| Positions | Bytecode | Java |
|---|---|---|
| 0 to 1 | Push 0, store it in slot 1 | int total = 0; |
| 2 to 3 | Push 1, store it in slot 2 | int i = 1; |
| 4 to 6 | Push i and n; if i > n, jump to 19 | The loop test i <= n, written as its opposite |
| 9 to 12 | Push total and i, add them, store in slot 1 | total += i; |
| 13 | Add 1 to slot 2 directly | i++ |
| 16 | Jump back to 4 | Repeat the loop |
| 19 to 20 | Push total, return it | return total; |
There is no "for" instruction. Every loop and if becomes conditional jumps and goto. The compiler tests the opposite condition (if_icmpgt for <=) so that it jumps out of the loop when the condition fails.
The Five Invoke Instructions
| Instruction | Used for | How the target is chosen |
|---|---|---|
invokestatic | Static methods | Fixed at link time; no object involved |
invokevirtual | Instance methods of a class | At run time, from the actual class of the object (overriding) |
invokeinterface | Methods called through an interface type | At run time, by searching the object's class for the interface method |
invokespecial | Constructors (<init>), super.method() calls | Fixed: exactly the named method, with no overriding |
invokedynamic | Lambdas, string concatenation, pattern switches, record methods | A bootstrap method links the call the first time it runs |
Let's see an example that uses all five:
import java.util.ArrayList;
import java.util.List;
public class Calls {
private int count;
static int square(int n) {
return n * n;
}
private void bump() {
count++;
}
public static void main(String[] args) {
int nine = square(3);
Calls c = new Calls();
c.bump();
List<String> names = new ArrayList<>();
names.add("Navika");
String text = names.toString();
Runnable r = () -> System.out.println(text + " " + nine);
r.run();
}
}[Navika] 9 0: iconst_3
1: invokestatic #13 // Method square:(I)I
4: istore_1
5: new #8 // class Calls
8: dup
9: invokespecial #17 // Method "<init>":()V
12: astore_2
13: aload_2
14: invokevirtual #18 // Method bump:()V
17: new #21 // class java/util/ArrayList
20: dup
21: invokespecial #23 // Method java/util/ArrayList."<init>":()V
24: astore_3
25: aload_3
26: ldc #24 // String Navika
28: invokeinterface #26, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
33: pop
34: aload_3
35: invokeinterface #32, 1 // InterfaceMethod java/util/List.toString:()Ljava/lang/String;
40: astore 4
42: aload 4
44: iload_1
45: invokedynamic #36, 0 // InvokeDynamic #0:run:(Ljava/lang/String;I)Ljava/lang/Runnable;
50: astore 5
52: aload 5
54: invokeinterface #40, 1 // InterfaceMethod java/lang/Runnable.run:()V
59: returnExplanation:
invokestatic(1):square(3). The argument 3 is pushed, the call pops it and pushes the result, whichistore_1saves innine.new,dup,invokespecial(5 to 9): creating an object takes three steps.newallocates memory and pushes a reference,dupcopies it, andinvokespecialruns the constructor<init>on one copy, leaving the other to be stored.invokevirtual(14):c.bump(). The object reference is pushed first, like a hidden first argument. (Before Java 11, private methods were called withinvokespecial.)invokeinterface(28, 35, 54):nameshas the typeList, an interface, so calls through it useinvokeinterface, even fortoString(). Thepopat 33 throws awayadd's unusedbooleanresult.invokedynamic(45): the lambda. The two captured variables,textandnine, are pushed, and the call returns aRunnable. The first time it runs, the bootstrap methodLambdaMetafactory.metafactorycreates the object's class; later runs reuse that link.
Why Invokedynamic Matters
The first four instructions have their behavior fixed by the JVM. invokedynamic lets Java code decide how a call site is linked, through a bootstrap method that returns a method handle (see "Method Handles in Java"). That gives the language room to improve without new bytecode: string concatenation moved to invokedynamic in Java 9 and got faster, and records, pattern-matching switches and lambdas all rely on it.
Common Mistakes
Assuming bytecode is what the CPU runs. It is the input to the interpreter and the JIT, which then optimize it (see "JIT Optimizations in Java").
Judging performance by counting instructions. After inlining and escape analysis, the machine code can look very different.
Forgetting slot 0 is this. In instance methods, the first parameter is in slot 1.
Expecting boolean instructions. boolean, byte, char and short are handled as int on the stack.
Interview Corner
What are the invoke instructions in JVM bytecode?
There are five. invokestatic calls static methods. invokevirtual calls instance methods with dynamic dispatch based on the object's actual class. invokeinterface calls methods through an interface reference. invokespecial calls constructors and super methods exactly, without dispatch. invokedynamic (Java 7) defers linking to a bootstrap method at first execution and is used for lambdas, string concatenation, records and pattern-matching switches.
What does it mean that the JVM is a stack machine?
Its instructions don't name registers. They take operands from an operand stack and push results back: to add two locals, the code loads both onto the stack (iload_1, iload_2), runs iadd, and stores the result (istore_3). Each method call has a frame with a local variable array and an operand stack, whose maximum sizes the compiler records in the class file.
Why are new and the constructor call separate instructions?
new only allocates the object and pushes a reference to uninitialized memory. The constructor is an ordinary method named <init> that must be called with invokespecial. The dup between them keeps a second reference on the stack, because the constructor call consumes one.
Practice
- Easy: Compile a method
static long twice(long x) { return x * 2; }and find thel-prefixed instructions withjavap -c. - Harder: Write a method that calls
Object.toString()on a variable declared asObject, one declared asList, and one viasuper.toString()inside a class. Predict the invoke instruction for each, then check.
Key Takeaways
Was this tutorial helpful?