Bytecode and javap

Java · Expert · 15 min

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>) and super. calls: no dynamic dispatch. (Since Java 11, calls to private methods use invokevirtual, a side effect of nestmates.)
  • invokedynamic — the call site is linked at runtime by a bootstrap method. Lambdas use it (via LambdaMetafactory), 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 method

Remember

  • 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 — javap shows they're erased to casts.

Check yourself

  1. An override calls `super.toString()`. Which instruction does javac emit for that call?
  2. How is a lambda expression implemented in bytecode?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html