Course contents

From source to bytecode to running program

Follow one tiny program from javac to the JIT: the class file, the constant pool, class loaders, linking, initialization and tiered compilation.

FreeLesson11 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: two compilers, one pipeline
  2. Under the hood: javac and the class file
  3. Class loading: who finds the bytes
  4. Linking and initialization
  5. Execution: interpreter, then tiered JIT
  6. Real world: the shortcut and the failures you'll meet
  7. Advanced: what candidates miss
  8. How to say it in the interview

⚙️ What happens between source code and a running program?

A .java file goes through two compilers and one loader before anything runs. javac turns source into bytecode inside a .class file. The JVM then loads, links and initializes that class, and its execution engine interprets the bytecode before the JIT compiles hot methods to native code. A three-line Hello becomes a 416-byte class file that starts with the four bytes CA FE BA BE.

For this lesson, we'll walk one tiny program through every stage and look at the real artifacts: the bytes, the javap listing and the class loaders. Interviewers ask "what happens when you run a Java program?" because it's open-ended: a junior answer stops at "javac then java", and a senior answer keeps going for five minutes. By the end, you'll be able to keep going.

Below the line (out of scope)

  • Garbage collection and heap layout
  • Writing custom class loaders and bytecode manipulation libraries
  • JVM tuning flags beyond the ones named here

The 30-second answer

javac compiles source into platform-neutral bytecode in .class files: a magic number, a version, a constant pool of symbolic references and the method bytecode. java starts the JVM, which loads classes on demand through the bootstrap, platform and application class loaders (parent first), links them (verify, prepare, resolve), initializes them by running static initializers, then executes. Execution starts in the interpreter; hot methods are compiled by C1 and later C2 through tiered compilation.

What you’ll learn

  • What javac puts in a .class file: the CAFEBABE magic, the version and the constant pool
  • How to read javap -c output and why bytecode is stack-based
  • How the bootstrap, platform and application class loaders delegate parent first
  • What verify, prepare, resolve and initialize each do, and when <clinit> runs
  • How the interpreter, C1 and C2 share the work, and what java Hello.java does since Java 11

01Picture it first: two compilers, one pipeline

Think of sheet music. The composer writes the piece (source), a publisher prints it in standard notation (bytecode) that any orchestra can read, and each orchestra performs it on its own instruments (the JVM on your OS and CPU). After a few rehearsals, the musicians stop reading the score for the passages they play most and play them from memory: that's the JIT.

The diagram splits the journey into compile time and runtime:

Fig. 1

javac turns Hello.java into Hello.class at compile time; at runtime the JVM loads, links, initializes and executes it, moving hot code from the interpreter to C1 and C2

javac turns Hello.java into Hello.class at compile time; at runtime the JVM loads, links, initializes and executes it, moving hot code from the interpreter to C1 and C2

Keep this order in your head: load, link, initialize, execute. Almost every follow-up question maps to one of those four boxes.

02Under the hood: javac and the class file

Try it yourself first

Before reading on, guess what javac produces for System.out.println("Hello, Atlas"). Machine code? A call to a C function? Something else?

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Atlas");
    }
}

Compile it with JDK 21, then disassemble with javap -c Hello:

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

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7   // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #13  // String Hello, Atlas
       5: invokevirtual #15  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

Notice four things:

  • A constructor appeared. We never wrote Hello(), but javac added the default constructor that calls Object.<init>.
  • Bytecode is stack-based. getstatic pushes System.out, ldc pushes the string, invokevirtual pops both and calls println. There are no registers.
  • The #7, #13, #15 are constant pool indexes, not addresses. The class file says "the field named out of type PrintStream in class java/lang/System" as text. The JVM turns that into a real pointer later, during resolution.
  • Offsets jump (0, 3, 5, 8) because instructions have different lengths: getstatic takes a 2-byte operand, ldc a 1-byte one.

The file itself has a fixed layout defined in chapter 4 of the JVM Specification. Run xxd Hello.class | head -1 and you'll see cafe babe 0000 0041 001d:

Bytes Field Our value
CA FE BA BE magic number identifies a class file
00 00 / 00 41 minor / major version 0 / 65 (Java 21; Java 17 is 61, Java 8 is 52)
00 1D constant pool count 29, meaning 28 entries (indexes start at 1)
… access flags, this class, super class, interfaces public, Hello, java/lang/Object
… fields, methods, attributes 0 fields, 2 methods, SourceFile

javap -v Hello prints the whole constant pool: UTF-8 strings like java/lang/System, Class entries pointing at them, NameAndType pairs, and Fieldref/Methodref entries combining both. That's the symbol table the JVM links against, and it's why one class file can run against any compatible library version.

03Class loading: who finds the bytes

A class is loaded the first time it's needed, not at startup. Three built-in loaders cooperate, and each has a parent:

  • Bootstrap loader: native code inside the JVM. Loads java.base and other core modules. In Java it shows up as null.
  • Platform loader (Java 9+, replaced the old "extension" loader): loads platform modules such as java.sql and java.net.http.
  • Application loader (also called the system loader): loads your classes and libraries from the class path and module path.

They follow parent-first delegation: asked to load a class, a loader first asks its parent, and only searches its own locations if the parent can't find it. That's how nobody can slip a fake java.lang.String onto the class path.

Fig. 2

A load request travels up from the application loader to the platform and bootstrap loaders, and each child searches its own locations only if its parent fails

A load request travels up from the application loader to the platform and bootstrap loaders, and each child searches its own locations only if its parent fails
public class Loaders {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(java.sql.Connection.class.getClassLoader());
        System.out.println(Loaders.class.getClassLoader());
        System.out.println(Loaders.class.getClassLoader().getParent());
    }
}

On JDK 21 this prints null, then ...ClassLoaders$PlatformClassLoader@..., then ...ClassLoaders$AppClassLoader@..., then the platform loader again. Since Java 9 the built-in loaders first check which module owns the package and go straight to that module's loader, but the visible effect is the same as classic delegation.

A class's runtime identity is its name plus its defining loader. Two loaders can each define com.acme.Order, and the JVM treats them as unrelated types.

04Linking and initialization

After loading, the class is linked in three steps (JVMS chapter 5):

  1. Verify: the verifier checks the bytecode is type-safe: no stack underflow, no jumping into the middle of an instruction, no treating an int as a reference. Since class version 50 it uses the StackMapTable attribute that javac emits, so verification is a fast single pass.
  2. Prepare: memory for static fields is allocated and set to default values (0, false, null). Your initial values are not assigned yet.
  3. Resolve: symbolic references from the constant pool become direct references. HotSpot does this lazily, the first time each instruction runs.

Then comes initialization: the JVM runs the class's <clinit> method, which holds static field initializers and static {} blocks in textual order. It happens exactly once, on first active use (new, a static method call, a non-constant static field access, reflection, or initializing a subclass), and the JVM holds a lock while it runs, so it's thread-safe.

Try it yourself first

Predict the output, line by line, before reading on.

public class InitOrder {
    public static void main(String[] args) {
        System.out.println("main starts");
        System.out.println(Config.LIMIT);
        System.out.println(Config.name);
        System.out.println(Config.name);
    }
}

class Config {
    static final int LIMIT = 10;
    static String name = load();

    static {
        System.out.println("Config initialized");
    }

    static String load() {
        System.out.println("load() runs");
        return "atlas";
    }
}

It prints main starts, 10, load() runs, Config initialized, atlas, atlas. Notice that reading LIMIT did not initialize Config: it's a compile-time constant, so javac copied 10 straight into InitOrder's bytecode (bipush 10). Reading name triggers <clinit>, which runs load() and then the static block in source order, once.

05Execution: interpreter, then tiered JIT

HotSpot starts every method in a template interpreter, which is quick to start but slow per instruction. Meanwhile it counts invocations and loop iterations. Tiered compilation (the default since Java 8) moves hot code up through levels:

Tier Who What it does
0 Interpreter Runs bytecode, collects counts
3 C1 with full profiling Compiles quickly, records types and branch frequencies (after about 200 calls, Tier3InvocationThreshold)
4 C2 Aggressive optimisation using the profile: inlining, escape analysis, loop unrolling (after about 5,000 calls, Tier4InvocationThreshold)
1, 2 C1 variants Trivial methods, or when C2's queue is busy

Long-running loops switch to compiled code mid-loop through on-stack replacement (OSR). If an optimistic assumption breaks (say, a second implementation of an interface gets loaded), the JVM deoptimizes back to the interpreter and recompiles later. Run with -XX:+PrintCompilation to watch methods climb the tiers.

06Real world: the shortcut and the failures you'll meet

Since Java 11 (JEP 330) you can skip the explicit javac step for a single file:

java Hello.java                    # compiles in memory, runs, writes no .class file
java --source 21 Hello.java a b    # extra words become args

The launcher compiles the file in memory and runs the first top-level class, which must declare main. Since Java 22 (JEP 458) the source launcher can also pull in other .java files from the same directory tree. It's great for scripts and teaching; real projects still use a build tool.

In the real world

The two loading failures I see most in production are version and visibility. UnsupportedClassVersionError: class file version 65.0, this version of the Java Runtime only recognizes class file versions up to 61.0 means a library built for Java 21 landed on a Java 17 runtime; fix it with javac --release 17 or upgrade the runtime. The other is ClassCastException: com.acme.Order cannot be cast to com.acme.Order in app servers and plugin systems, which means the same class name was defined by two different loaders.

07Advanced: what candidates miss

Where candidates slip

Many candidates say the JVM "loads all classes at startup" or "compiles everything to machine code before running". Both are wrong. Loading is lazy and per class, and JIT compilation is selective: most methods in a large service are never compiled at all. Saying "load, link, initialize, on first use" and "interpreter first, C1 then C2 for hot code" is what separates a memorised answer from an understood one.

Performance angle

This pipeline is why Java services have a warm-up curve: the first few thousand requests run partly interpreted while C1 and C2 catch up, and latency can be several times worse than at steady state. The default CDS archive (since JDK 12) cuts class loading cost at startup, and Java 24's AOT class loading and linking (JEP 483) goes further by caching loaded and linked classes from a training run. My default: measure warm-up first, then reach for CDS/AOT caches before tuning JIT thresholds.

08How to say it in the interview

When I ask "what happens when you run a Java program?", I'm listening for the order of the stages, one concrete detail from inside each (constant pool, delegation, <clinit>, tiers), and whether you know laziness is the rule. If you name a production symptom for any stage, you're clearly above the bar.

In the interview

"javac compiles source into bytecode in a class file: magic CAFEBABE, a version, a constant pool of symbolic references, and stack-based bytecode for each method. When I run java, the launcher starts the JVM and the application class loader loads my main class, delegating to the platform and bootstrap loaders first. The class is linked (verified, static fields prepared to defaults, references resolved lazily), then initialized by running its static initializers once. Execution starts in the interpreter, and hot methods are compiled by C1 with profiling and then by C2, which is why Java gets faster after warm-up."

On the whiteboard

.java -> .class -> running

  • javac: source -> bytecode (CAFEBABE, v65, constant pool)
  • load: bootstrap <- platform <- app (parent first)
  • link: verify, prepare (defaults), resolve (lazy)
  • init: once, on first active use
  • execute: interpreter -> C1 (tier 3) -> C2 (tier 4)
  • java Hello.java: in-memory compile (Java 11+)
Practise this topicHow Java runs: interview questions & scenarios
Further reading (4)