Bytecode

Bytecode is the platform-neutral instruction set the JVM actually executes — the compiled form every Java source file becomes, and the layer where the operand stack, local variables, and constant pool all meet.

When javac compiles a .java file, it does not produce native machine code — it produces bytecode, a compact, platform-neutral instruction set stored in a .class file, designed specifically for a stack-based virtual machine to execute (either by interpreting it directly or JIT-compiling it to native code, as covered in the JVM Architecture topic). Understanding bytecode's basic shape — its instruction format, its use of an operand stack and a local variable array, and how it references the runtime constant pool — demystifies a lot of otherwise-opaque Java behavior, and the javap tool lets you inspect it directly from any compiled .class file.

What Bytecode Is

Bytecode is a sequence of one-byte opcodes (hence the name), each optionally followed by operand bytes, stored inside a .class file's code attribute for each method. It is not native machine code for any particular CPU — it's an abstract instruction set for the JVM's own stack-based virtual machine, designed to be compact, verifiable, and reasonably straightforward to interpret or JIT-compile on any real hardware.

'Bytecode' gets its name because most opcodes fit in a single byte, allowing up to 256 distinct instructions (roughly 200 are actually defined) — a deliberately compact format compared to variable-length or wider native instruction encodings.

The Stack-Based Execution Model

Unlike a register-based machine (where instructions typically name specific registers to operate on), the JVM is stack-based: most instructions take their operands from, and push their results onto, an operand stack that's part of the current method's stack frame (see the JVM Runtime Data Areas topic). This trades some raw execution efficiency for a simpler, more compact, and easier-to-verify instruction encoding — instructions rarely need to specify operand locations explicitly, since they implicitly work on whatever's on top of the stack.

A Simple Example: Compiling and Disassembling

Take a minimal method and see what bytecode javac actually produces for it.

javap -c disassembles compiled bytecode into a readable mnemonic form. add's bytecode: iload_0 pushes parameter a onto the operand stack, iload_1 pushes parameter b, iadd pops both, adds them, and pushes the result, and ireturn pops that result and returns it to the caller.

iload_0: push local variable slot 0 (parameter a) onto the operand stack.

iload_1: push local variable slot 1 (parameter b) onto the operand stack.

iadd: pop the top two int values, add them, push the sum.

ireturn: pop the top int value and return it as the method's result.

public class Add {
    public static int add(int a, int b) {
        return a + b;
    }
}
$ javac Add.java
$ javap -c Add.class

public class Add {
  public Add();
    Code:
       0: aload_0
       1: invokespecial #1  // Method java/lang/Object."<init>":()V
       4: return

  public static int add(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: iadd
       3: ireturn
}

Instruction Naming Conventions

Bytecode mnemonics follow a fairly consistent prefix convention indicating the operand type they work on: i for int, l for long, f for float, d for double, a for reference (object/array). So iload loads an int local variable, aload loads a reference, dstore stores a double, and so on. This is because the JVM's operand stack and instruction set are, in effect, statically typed per-instruction even though the stack itself has no single declared type — the compiler chooses the right typed instruction based on the static type of the expression it's compiling.

iload / istore   - int local variable load/store
lload / lstore   - long local variable load/store
fload / fstore   - float local variable load/store
dload / dstore   - double local variable load/store
aload / astore   - reference (object/array) local variable load/store
iadd, isub, imul, idiv  - int arithmetic (operands come from the operand stack)
invokevirtual    - invoke an instance method (dynamic dispatch)
invokestatic     - invoke a static method
invokespecial    - invoke a constructor, private method, or superclass method
invokeinterface  - invoke an interface method
getfield/putfield - read/write an instance field
getstatic/putstatic - read/write a static field

The Operand Stack

The operand stack is a per-frame, per-invocation working area (see the JVM Runtime Data Areas topic) that most bytecode instructions push to and pop from. Its maximum depth needed by a given method is computed by the compiler and recorded in the class file (max_stack), so the JVM can allocate exactly enough space up front rather than needing a dynamically resizable structure at run time.

The Local Variable Array

Each frame also has a local variable array — indexed slots holding a method's parameters and local variables. For an instance method, slot 0 always holds the this reference; parameters follow in declaration order, then local variables as they're declared in the method body. long and double values occupy two consecutive slots (since they're 64-bit values on a 32-bit-slot-oriented array), while every other type occupies one slot.

class Example {
    void method(int x, String s) {
        double d = 1.5;
    }
}
// local variable slots:
// 0: this
// 1: x
// 2: s
// 3-4: d (double occupies two consecutive slots)

Constant Pool References in Bytecode

Bytecode instructions don't embed method names, field names, or class names inline — they reference entries in the class's runtime constant pool by index (see the JVM Runtime Data Areas topic). This is why javap -c output shows comments like // Method java/lang/Object."<init>":()V next to an invokespecial instruction — the instruction itself just carries a constant pool index; javap resolves and prints the human-readable form for you.

2: invokevirtual #7  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

Control Flow in Bytecode

Higher-level control flow constructs (if, for, while, switch) compile down to conditional and unconditional jump instructions operating on raw bytecode offsets — there is no if or for at the bytecode level, only comparisons and branches.

ifge is a conditional branch: if the top-of-stack int is >= 0, execution jumps to offset 7 (return n unchanged); otherwise it falls through to negate and return -n. This is exactly how source-level if statements compile down.

static int abs(int n) {
    if (n < 0) {
        return -n;
    }
    return n;
}
0: iload_0
1: ifge     7        // if n >= 0, jump to offset 7
4: iload_0
5: ineg
6: ireturn
7: iload_0
8: ireturn

The Class File Concept

A .class file is the binary container format holding bytecode along with everything needed to load and verify it: a magic number (0xCAFEBABE) identifying it as a class file, version numbers, the constant pool, access flags, superclass/interface references, field and method descriptors, each method's bytecode (in its Code attribute), and additional metadata attributes (like line-number tables for debugging, or annotations).

Disassembly with javap

javap, bundled with the JDK, disassembles compiled .class files without needing the original source. Its most useful flags:

javap is invaluable for understanding exactly what the compiler produced for a given piece of source — confirming whether a loop was actually unrolled, checking how autoboxing compiles, or verifying which invoke* instruction a call site resolved to (e.g. invokedynamic for lambda expressions, which compile very differently from ordinary method calls).

javap ClassName            # public members' signatures only
javap -p ClassName          # include private members too
javap -c ClassName          # disassemble bytecode instructions
javap -v ClassName          # verbose: constant pool, flags, and everything else
javap -l ClassName          # include line-number and local-variable tables

Key Takeaways

Bytecode is the platform-neutral instruction set the JVM executes — produced by javac, stored in .class files, and either interpreted or JIT-compiled at run time, never native machine code by itself.

The JVM is stack-based: most instructions operate implicitly on an operand stack rather than naming registers explicitly.

Instruction mnemonics encode their operand type by prefix (i/l/f/d/a for int/long/float/double/reference).

Instructions reference class, method, and field names indirectly through the constant pool by index, not inline.

javap disassembles any compiled .class file into readable bytecode, invaluable for understanding exactly what the compiler produced.

Assuming bytecode is native machine code that runs directly on the CPU.

Understand bytecode as an abstract, platform-neutral instruction set for the JVM's own stack-based virtual machine, which the JVM interprets or JIT-compiles to real native code at run time.

Conflating bytecode with native machine code misunderstands the entire point of 'write once, run anywhere' — the same bytecode runs on any CPU architecture with a compatible JVM.

Expecting method/field/class names to appear literally embedded in bytecode instructions.

Recognize that instructions reference such names indirectly via constant pool indices, which javap resolves and displays as comments for readability.

The actual bytecode bytes contain only opcodes and small numeric operands (like constant pool indices), not embedded strings for names.

Assuming Java's if/for/while/switch constructs have direct one-to-one bytecode instruction equivalents.

Recognize that they all compile down to comparison and (conditional/unconditional) jump instructions over raw bytecode offsets — there is no dedicated 'if' or 'for' instruction.

Bytecode has no structured control-flow instructions; all higher-level control flow is synthesized from branches, which is exactly why decompilers have real work to do reconstructing readable control structures.

What does javac actually produce when it compiles a .java file, and where is it stored?

Bytecode — a platform-neutral instruction set for the JVM — stored inside a .class file's per-method Code attribute.

Is the JVM a register-based or stack-based virtual machine, and what does that mean for how most instructions work?

Stack-based. Most instructions implicitly take their operands from, and push their results onto, an operand stack in the current method's frame, rather than naming specific registers.

What does the 'i' prefix in instructions like iload, istore, and iadd indicate?

That the instruction operates on an int value; other prefixes indicate other types (l for long, f for float, d for double, a for reference).

Why does invokespecial appear for constructor calls instead of invokevirtual?

invokespecial is used for constructors, private methods, and superclass method calls — cases where the exact method to invoke is known statically and dynamic dispatch isn't needed, unlike invokevirtual which supports polymorphic dispatch on instance methods.

How do bytecode instructions refer to class, method, and field names?

Indirectly, via numeric indices into the class's runtime constant pool, rather than embedding the names directly in the instruction bytes.

How does a source-level if statement get represented in compiled bytecode?

As a comparison instruction followed by a conditional jump (branch) to a specific bytecode offset — there is no dedicated 'if' instruction; all structured control flow is built from branches over raw offsets.

What tool lets you disassemble a compiled .class file to see its actual bytecode instructions, and what flag shows the instructions themselves?

javap, using the -c flag (javap -c ClassName) to print disassembled bytecode; -v gives fuller verbose output including the constant pool.

Without running javap, describe in words what kinds of bytecode instructions you'd expect to see for the for loop's condition check and increment.

The condition (i < arr.length) compiles to loading i and arr.length onto the operand stack, comparing them, and a conditional branch (something like if_icmpge jumping past the loop body when the condition is false). The increment (i++) compiles to an iinc instruction (a compact dedicated instruction for incrementing a local int variable by a constant) followed by an unconditional jump (goto) back to the condition check.

Write and disassemble (describe the expected javap -c output conceptually) a method that returns a hardcoded String literal, to illustrate a constant pool reference.

public class Greeting { public static String get() { return "hello"; } } javap -c would show something like: 0: ldc #2 // String hello, followed by 2: areturn. ldc loads a constant pool entry (here, the String literal "hello", referenced by index rather than embedded inline) onto the operand stack, and areturn pops and returns that reference to the caller.

Write the javap command to view both the disassembled bytecode and the full constant pool of a compiled class named Calculator.class.

javap -v -c Calculator.class -v (verbose) prints the constant pool along with class-level metadata (flags, version, etc.), and -c disassembles every method's bytecode instructions; combined, this gives the complete picture of what the compiler produced for Calculator.

public class Sum {
    public static int sum(int[] arr) {
        int total = 0;
        for (int i = 0; i < arr.length; i++) {
            total += arr[i];
        }
        return total;
    }
}