☕ Java

Java Execution Flow

What happens between typing java MyProgram and seeing output on screen? Java's execution flow is a precisely ordered sequence of events — class loading, verification, interpretation, JIT compilation, and garbage collection — all working together to run your program correctly and efficiently.

The Execution Pipeline at a Glance

Java's execution flow starts the moment you invoke the java command and ends when the main method returns. Between those two points, six distinct systems work in sequence and in parallel — the class loader, verifier, interpreter, JIT compiler, garbage collector, and the JVM's internal thread scheduler. Understanding this flow explains why Java behaves the way it does: why it starts slower than native binaries but speeds up over time, why stack traces show exact line numbers, why certain errors only appear at runtime, and why long-running Java servers consistently outperform short-lived processes.

Step 1 — JVM Startup

When you run java HelloWorld, the OS launches the JVM process. The JVM initializes its internal subsystems in order: - Memory areas are allocated: heap, method area, stack space per thread - The bootstrap class loader is initialized — it loads the core Java classes (java.lang.Object, java.lang.String, etc.) from the JDK's rt.jar or modules - The main thread is created - System properties and environment variables are set This all happens before your code runs. On a modern machine it takes milliseconds, but it's measurable — which is why spinning up a new JVM for a short script feels slower than running a native binary.

Step 2 — Class Loading

The JVM's Class Loader locates and loads the class you specified — HelloWorld — into memory. Java uses a three-tier class loader hierarchy: - Bootstrap ClassLoader — loads core Java classes (java.lang.*, java.util.*) - Extension/Platform ClassLoader — loads classes from the JDK's extension directory - Application ClassLoader — loads your application's classes from the classpath Class loading is lazy: a class is loaded only when it's first referenced. Importing a class doesn't load it — instantiating it or calling a static method does. This keeps startup fast and memory usage proportional to what's actually used.
Shell
// You can observe class loading with a JVM flag:
java -verbose:class HelloWorld

// Output shows each class as it's loaded:
// [Loaded java.lang.Object from /path/to/jdk/...]
// [Loaded java.lang.String from /path/to/jdk/...]
// [Loaded HelloWorld from file:/path/to/your/project/]

Step 3 — Bytecode Verification

Before executing any bytecode, the JVM's Bytecode Verifier performs a static analysis pass. It confirms: - The .class file starts with the magic number 0xCAFEBABE - All bytecode instructions are valid and well-formed - Type safety is never violated — no integer treated as a reference - The operand stack never overflows or underflows - No unauthorized access to private fields or methods - All jumps land on valid instruction boundaries If any check fails, a VerifyError is thrown and execution stops. This verification step is what allows the JVM to safely run bytecode from untrusted sources — it's structurally impossible for unverified code to execute.

Step 4 — main() Method Invocation

After the class is loaded and verified, the JVM looks for the entry point — a method with this exact signature:
Java
public static void main(String[] args)

// Every word matters:
// public  — JVM calls it from outside the class
// static  — called without creating an instance first
// void    — returns nothing to the JVM
// main    — the exact name the JVM looks for
// String[] args — command-line arguments (empty array if none provided)

// Pass arguments at the command line:
java HelloWorld Alice 30
// Inside main: args[0] = "Alice", args[1] = "30"

Step 5 — Interpretation and JIT Compilation

The Execution Engine runs your bytecode using two complementary strategies that work together automatically: The Interpreter executes bytecode immediately, instruction by instruction. No warm-up needed — execution starts right away. The downside: it re-translates the same bytecode every time the same method runs. The JIT Compiler monitors method call frequency at runtime. Once a method crosses the compilation threshold (around 10,000 invocations in HotSpot), the JIT compiles it to optimized native machine code — tailored to the exact CPU it's running on. That native code is cached and reused on every subsequent call. The JVM uses tiered compilation: methods start interpreted, move to a lightly optimized compiled form, then to a fully optimized native form as they prove their hotness. The longer your JVM runs, the more code gets JIT-compiled, and the faster it gets.
Shell
// Watch JIT compilation in action:
java -XX:+PrintCompilation HelloWorld

// Sample output:
//    42    1    3    java.lang.String::hashCode (55 bytes)
//    67    2    4    java.lang.String::equals (44 bytes)
//   102    3    3    HelloWorld::main (10 bytes)
//
// Columns: timestamp | id | tier | method (size)
// Tier 3 = client compiler, Tier 4 = server compiler (most optimized)

Step 6 — Garbage Collection (Concurrent)

While your program executes, the Garbage Collector runs concurrently in background threads. It continuously tracks object reachability — starting from "GC roots" (static fields, local variables in active stack frames, JNI references) and tracing all reachable objects. Anything not reachable is garbage. The GC collects in generations: - Minor GC — collects the Young Generation (Eden + Survivor spaces). Fast, frequent, typically sub-millisecond. - Major GC — collects the Old Generation. Slower, infrequent. - Full GC — collects everything including Metaspace. Rare, should be investigated if frequent. The GC runs mostly concurrently with your application — it doesn't freeze the world for most of its work. Short "stop-the-world" pauses are still needed for certain phases, which is why GC tuning matters for latency-sensitive applications.

Step 7 — Program Termination

The JVM shuts down when one of these occurs: - The main method returns normally - System.exit(int status) is called (status 0 = success, non-zero = error) - An uncaught exception propagates out of every thread - The JVM process is killed externally Before shutdown, the JVM runs shutdown hooks — threads registered via Runtime.getRuntime().addShutdownHook(). These are used to flush buffers, close database connections, write final log entries, or clean up temporary files.
Java
// Register a shutdown hook:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("JVM shutting down — cleaning up...");
    // close connections, flush buffers, etc.
}));

// Force exit with a status code:
System.exit(0);   // success
System.exit(1);   // error — signals failure to the OS or calling script