Module 1 · Getting Started & Mental Models ⏱ 19 min

From .java to .class: Compilation, Bytecode, Runtime

By the end of this lesson you will be able to:
  • Trace the full path from `.java` source to compiled `.class` bytecode
  • Explain why bytecode is platform-independent but the JVM is not
  • Describe what happens when the JVM loads and executes a class
  • Recognise the forgotten-recompile trap and how the JIT compiler works

Java is neither fully compiled nor fully interpreted. It sits in the middle: source code is compiled into bytecode, and the JVM interprets or compiles that bytecode into native machine code at runtime. Understanding that two-step process explains why Java is portable, why compilation catches errors early, and why startup is slower than a pure scripting language. Every professional Java developer should be able to trace this pipeline in their head.

Step 1: write .java source

You write human-readable code in a file ending in .java. This is plain text — you could write it in Notepad — and it contains classes, methods, and statements. The compiler does not care about indentation or comments; it cares about syntax and types. Every public class must live in a file whose name matches the class exactly, including case. public class Main must be in Main.java; main.java will not compile.

Step 2: javac compiles to .class bytecode

The command javac Main.java reads your source and produces Main.class. That .class file is not machine code your CPU understands directly — it is bytecode, a compact intermediate instruction set designed for the JVM. Bytecode is platform-independent: the same .class file can run on Windows, macOS, or Linux without modification. The trade-off is that something else — the JVM — must translate those instructions into real CPU operations at runtime.

flowchart LR
  SRC["Main.java (source)"] --> JC["javac (compiler)"]
  JC --> CLS["Main.class (bytecode)"]
  CLS --> JVM["JVM loads and verifies"]
  JVM --> INT["Interpreter (cold code)"]
  JVM --> JIT["JIT compiler (hot code)"]
  INT --> OUT["Program output"]
  JIT --> OUT
  style JC fill:#c2410c,color:#fff
  style JVM fill:#c2410c,color:#fff
  style JIT fill:#c2410c,color:#fff
The full Java pipeline: source → bytecode → verification → interpreter or JIT → native code.
Main.java — a program we will trace through the full pipeline. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        int a = 4;
        int b = 6;
        int sum = a + b;
        System.out.println("Sum: " + sum);
        System.out.println("Double: " + (sum * 2));
    }
}

Step 3: the JVM loads and executes

The command java Main starts the JVM, which loads Main.class, verifies it, and begins executing the bytecode. Early in execution the JVM may use an interpreter to run bytecode line by line; for code that runs frequently, a JIT compiler translates the bytecode into native machine code so it runs at full speed.

The verification step is strict: the JVM checks that the bytecode does not violate type safety, does not access illegal memory, and does not corrupt the stack. This is part of Java's security story — untrusted code cannot harm the host process.

flowchart TD
  LOAD["Class loader"] --> VERIFY["Bytecode verifier"]
  VERIFY --> EXEC["Interpreter / JIT"]
  style LOAD fill:#c2410c,color:#fff
  style VERIFY fill:#166534,color:#fff
  style EXEC fill:#1e293b,color:#fff
Inside the JVM: classes are loaded, verified, then executed by the interpreter or JIT.

The JIT compiler and warm-up

The Just-In-Time (JIT) compiler watches which methods run most often and translates their bytecode into highly optimised native code. After JIT compilation, those hot methods run at roughly the same speed as C or C++. The catch is warm-up time: the JVM must observe your code long enough to decide what is worth compiling.

For long-running servers this cost is negligible; for short scripts it is why Java feels heavy compared to Python. The JIT is why Java benchmarks always specify a warm-up period — measuring cold JVM performance tells you nothing about steady-state speed.

Main.java — a program with a helper method to show how the JVM calls compiled bytecode. Real, compiling Java.
public class Main {
    static int doubleIt(int n) {
        return n * 2;
    }

    public static void main(String[] args) {
        int x = 5;
        System.out.println(doubleIt(x));
        System.out.println(doubleIt(x - 2));
    }
}

Classpath and class loading

When the JVM starts, it looks for classes on the classpath — a list of directories and .jar files where compiled classes live. The JVM does not search your entire hard drive; it looks only where you tell it to. A ClassNotFoundException almost always means the class exists but the classpath is wrong.

Classes are loaded on demand: the JVM loads Main first, then loads any class Main references only when that reference is actually reached during execution. Understanding this lazy loading model helps explain why a missing dependency only crashes when the code path that needs it is reached, not at startup.

flowchart TD
  A["Write Main.java"] --> B["javac Main.java"]
  B --> C["java Main"]
  C --> D["Edit Main.java"]
  D --> E["javac Main.java<br/>(don't forget!)"]
  E --> C
  style B fill:#166534,color:#fff
  style E fill:#b45309,color:#fff
The edit-compile-run cycle. Skipping the recompile step is the most common beginner mistake.

What bytecode looks like

You don't need to read bytecode fluently, but peeking at it once is illuminating. After compiling, javap -c Main disassembles the .class file and shows instructions like iconst_2, istore_1, and iadd. These are stack-based operations: push a value, store it, add two values. The JVM executes these instructions against its own stack machine, which is why the same bytecode runs on ARM, x86, or any architecture with a JVM port.

Exercise

What does this program print? Read it carefully and type the exact output (two lines).

public class Main {
    public static void main(String[] args) {
        int a = 4;
        int b = 6;
        int sum = a + b;
        System.out.println("Sum: " + sum);
        System.out.println("Double: " + (sum * 2));
    }
}
Exercise

What is the output of javac Main.java?

Exercise

Trace this simple program through the pipeline. Predict the exact two-line output.

public class Main {
    public static void main(String[] args) {
        int x = 3;
        int y = x * 4;
        System.out.println(y);
        System.out.println(y + x);
    }
}
Exercise

The JVM starts at main and follows method calls. Predict the exact two-line output.

public class Main {
    static int triple(int n) {
        return n * 3;
    }

    public static void main(String[] args) {
        int a = 2;
        System.out.println(triple(a));
        System.out.println(triple(a + 1));
    }
}
Exercise

You edit Main.java and run java Main, but the output does not reflect your changes. What is the most likely cause?

Recap

  • Java source (.java) is compiled by javac into platform-independent bytecode (.class).
  • The JVM loads, verifies, and executes bytecode, translating hot paths to native code via the JIT compiler.
  • Verification catches type and memory safety violations before execution begins.
  • The classpath tells the JVM where to find classes; ClassNotFoundException means the classpath is wrong.
  • Always recompile after editing source, or java will run the old bytecode.

Next you will learn how Java represents text with Strings, and why == on a String rarely does what you expect.

Checkpoint quiz

Why can the same .class file run on Windows, macOS, and Linux?

What does the JVM do before it starts executing loaded bytecode?

Go deeper — technical resources