Java source code compiles to bytecode — platform-independent instructions executed by the JVM. Understanding bytecode helps with debugging, performance analysis, and framework internals.

Compilation Pipeline

  Hello.java  →  javac  →  Hello.class (bytecode)  →  JVM  →  native code
  
  javac Hello.java
javap -c Hello          # disassemble bytecode
javap -verbose Hello    # full class details
  

Class File Structure

A .class file contains:

Section Contents
Magic number 0xCAFEBABE
Version Major/minor Java version
Constant pool Strings, class names, method refs
Access flags public, final, interface, etc.
This class / Super class Class hierarchy
Fields Field descriptors
Methods Method descriptors + bytecode
Attributes Source file, line numbers, etc.

Sample Disassembly

Source:

  public class Add {
    public int add(int a, int b) {
        return a + b;
    }
}
  

Bytecode (javap -c Add):

  public int add(int, int);
  Code:
     0: iload_1       // load local variable 1 (a) onto stack
     1: iload_2       // load local variable 2 (b) onto stack
     2: iadd          // pop two ints, push sum
     3: ireturn       // return int from top of stack
  

Operand Stack Model

The JVM is a stack-based machine. Most instructions operate on an operand stack:

  Stack: []
iload_1  →  [5]
iload_2  →  [5, 3]
iadd     →  [8]
ireturn  →  returns 8
  

Common Bytecode Instructions

Instruction Description
iconst_0..iconst_5 Push int constant 0-5
bipush, sipush Push byte/short constant
iload, istore Load/store int local variable
aload, astore Load/store reference
getfield, putfield Access instance field
getstatic, putstatic Access static field
invokevirtual Call instance method
invokestatic Call static method
invokeinterface Call interface method
invokedynamic Dynamic method call (lambdas)
new Create object
ifeq, ifne, goto Branch instructions
iadd, isub, imul Integer arithmetic
return, ireturn, areturn Return from method

invokedynamic and Lambdas

Lambdas compile to invokedynamic instead of anonymous inner classes:

  Runnable r = () -> System.out.println("Hello");
  

The bootstrap method links to a factory that generates the functional object at runtime — more efficient than creating a new class per lambda.

Viewing Bytecode with Tools

  # Command line
javap -c -p MyClass.class

# With JDK tools
jdeps MyClass.class          # module dependencies
jdeprscan --class-path . MyClass  # deprecated API usage
  

IDE plugins (IntelliJ “Show Bytecode”) and tools like Bytecode Viewer provide GUI access.

Practical Uses

  • Debugging — Understand what the JVM actually executes
  • Performance — Identify autoboxing, unnecessary object creation
  • Security — Audit what code actually runs
  • Framework internals — Spring, Hibernate, and Mockito rely heavily on bytecode manipulation (CGLIB, ASM, Javassist)

Best Practices

  • Use javap to inspect unexpected behavior or performance issues
  • Understand that lambdas use invokedynamic — not anonymous inner classes
  • Leave bytecode manipulation to established libraries (ASM, ByteBuddy)
  • Match bytecode knowledge with source-level understanding — don’t optimize prematurely