Reading bytecode settles debates about what the compiler actually generates and is the foundation for agents, profilers and code generation.
javac compiles to bytecode, the instruction set of the JVM. Inspect it with javap -c -p MyClass.class (-v adds the constant pool and more).
The JVM is a stack machine. Each method frame has an array of local variable slots (slot 0 is this in instance methods) and an operand stack. a + b for ints becomes iload_1, iload_2, iadd, istore_3. Instruction prefixes encode types: i int, l long, d double, a reference.
Method calls come in five flavours, and knowing them explains a lot of language behaviour:
invokestatic— static methods.invokevirtual— ordinary instance methods, dispatched on the runtime class (polymorphism).invokeinterface— calls through an interface type.invokespecial— constructors (<init>) andsuper.calls: no dynamic dispatch. (Since Java 11, calls to private methods useinvokevirtual, a side effect of nestmates.)invokedynamic— the call site is linked at runtime by a bootstrap method. Lambdas use it (viaLambdaMetafactory), and so does string concatenation since Java 9 (StringConcatFactory).
Other details become visible too: generics are erased to their bounds with checkcast inserted at use sites; autoboxing turns into Integer.valueOf and intValue() calls; a switch on strings becomes a hashCode lookup followed by equals; constructors and static initializers appear as <init> and <clinit>.
The class file also carries the constant pool (names, literals, method references), a version number (65 for Java 21) and attributes such as line number tables that power stack traces.
Libraries like ASM and ByteBuddy generate and rewrite bytecode — that's how Mockito, Hibernate proxies and Java agents work. A standard Class-File API arrived as a preview after Java 21.
Example
import java.util.function.IntBinaryOperator;
public class Calc {
static int add(int a, int b) { return a + b; }
public static void main(String[] args) {
IntBinaryOperator op = (x, y) -> x * y;
String s = "sum=" + add(2, 3) + " product=" + op.applyAsInt(2, 3);
System.out.println(s); // sum=5 product=6
}
}
// $ javac Calc.java && javap -c -p Calc
// static int add(int, int);
// 0: iload_0
// 1: iload_1
// 2: iadd
// 3: ireturn
// public static void main(java.lang.String[]);
// 0: invokedynamic #7, 0 // InvokeDynamic #0:applyAsInt:()Ljava/util/function/IntBinaryOperator;
// ...
// 10: invokestatic #11 // Method add:(II)I
// ...
// 22: invokedynamic #21, 0 // InvokeDynamic #1:makeConcatWithConstants:(II)Ljava/lang/String;
// private static int lambda$main$0(int, int); <- the lambda body becomes a private methodRemember
- The JVM is a stack machine: locals in slots, work on an operand stack
- invokestatic, invokevirtual, invokeinterface, invokespecial and invokedynamic encode call semantics
- Lambdas and string concatenation are linked at runtime through invokedynamic
Watch out for
- Assuming source-level features like generics exist at runtime —
javapshows they're erased to casts.