☕ Java

How Java Works

You write Java code, hit run, and something happens. But what exactly? Understanding how Java works end-to-end — from source file to running program — gives you a mental model that makes you a better debugger, a better architect, and a developer who actually knows what's happening under the hood.

The Big Picture

Most developers know Java runs on a JVM. Far fewer can explain what actually happens between typing java HelloWorld and seeing output on the screen. There are six distinct stages — each with a specific job. Miss any one of them and the program doesn't run. Here's the full pipeline: Write → Compile → Load → Verify → Execute → Garbage Collect. Let's walk through each one with real detail.

Stage 1 — Writing Source Code (.java)

Everything starts with a .java file. Java source code is plain text — human-readable instructions written according to Java's syntax rules. Each public class must live in its own file with a matching name. A few things worth noting at this stage: - Java is strongly typed — every variable has a declared type, checked at compile time - Java is case-sensitive — String and string are different things - The entry point of every Java application is always public static void main(String[] args) Nothing runs yet. This is just text on disk.
Java
// File: HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        String message = "Hello, World!";
        System.out.println(message);
    }
}

Stage 2 — Compilation (javac): .java → .class

When you run javac HelloWorld.java, the Java compiler kicks in. It does several things in sequence: 1. Lexical analysis — Breaks your source code into tokens (keywords, identifiers, operators, literals) 2. Syntax analysis — Checks that the tokens form valid Java grammar (builds a parse tree) 3. Semantic analysis — Checks types, resolves variable references, validates method signatures 4. Bytecode generation — Produces HelloWorld.class containing JVM bytecode If any step fails — a typo, a type mismatch, a missing semicolon — the compiler throws an error and produces no output. This is Java's first line of defense: a large class of bugs never make it past the compiler. The output (.class file) is not machine code. It's platform-neutral bytecode — the same file runs on Windows, Linux, or macOS without modification.
Shell
// Compile:
javac HelloWorld.java

// Output: HelloWorld.class (bytecode — not human-readable, not machine code)

// You can inspect the bytecode with javap:
javap -c HelloWorld

// Output:
// public static void main(java.lang.String[]);
//   Code:
//      0: ldc           #7   // push "Hello, World!" onto stack
//      2: astore_1           // store in local variable 1
//      3: getstatic     #9   // get System.out
//      6: aload_1            // load local variable 1
//      7: invokevirtual #15  // call println(String)
//     10: return

Stage 3 — Class Loading

When you run java HelloWorld, the JVM starts up and the Class Loader takes over. Its job: find the .class file, load it into memory, and prepare it for execution. Class loading happens in three phases: Loading — The Class Loader reads the .class file from disk (or a JAR, or the network) and creates a Class object in the JVM's method area representing it. Linking — Three sub-steps: - Verification: Is the bytecode structurally valid? Does it follow JVM rules? - Preparation: Allocate memory for static fields and set default values - Resolution: Replace symbolic references (like class names) with direct memory references Initialization — Run static initializers and assign declared values to static fields. This is when static { } blocks execute. Crucially, class loading is lazy by default. A class isn't loaded until it's first needed. This keeps startup time fast and memory usage low.

Stage 4 — Bytecode Verification

Before executing a single instruction, the JVM's Bytecode Verifier runs a full analysis of the loaded bytecode. This is a security and stability guarantee — it ensures that no matter where the .class file came from, it won't corrupt the JVM. The verifier checks: - The class file format is valid (starts with magic number 0xCAFEBABE) - No instruction jumps to an invalid location - Method calls use the correct argument types and counts - Type safety rules are never violated - The operand stack never overflows or underflows - No illegal access to private fields or methods If verification fails, the JVM throws a VerifyError and refuses to run the code entirely. This is why Java has no buffer overflows at the language level — the verifier catches them before they can execute.

Stage 5 — Execution: Interpreter + JIT Compiler

Verified bytecode is handed to the Execution Engine — the part of the JVM that actually runs your program. It uses two complementary strategies: The Interpreter starts immediately. It reads bytecode instructions one by one and executes them. Simple, fast to start, but slow for repeated code — it re-translates the same instructions every time they run. The JIT Compiler (Just-In-Time) kicks in for hot code. The JVM continuously profiles your running program, tracking which methods are called frequently. When a method crosses a threshold (typically 10,000 calls in HotSpot), the JIT compiles it directly to optimized native machine code for the current CPU. That native code is cached — every subsequent call runs at full native speed. The result: Java starts quickly (interpreter) and gets faster over time (JIT). Long-running server applications — web backends, trading systems, data pipelines — often reach C++-level performance after a warm-up period because the JIT has had time to optimize the hot paths.
Shell
// The JVM automatically decides what to interpret vs JIT-compile.
// You can observe JIT compilation with JVM flags:

java -XX:+PrintCompilation HelloWorld

// Output (example):
//     42    1       3       java.lang.String::hashCode (55 bytes)
//     55    2       4       java.lang.String::equals (44 bytes)
//    102    3       3       HelloWorld::main (10 bytes)
//
// The numbers show: timestamp, compile ID, optimization tier, method name

Stage 6 — Garbage Collection

While your program runs, it constantly creates objects on the heap. A temporary String here, a request object there, a list that gets built and discarded. Without cleanup, the heap fills up and the program crashes. Java's Garbage Collector (GC) runs automatically in the background, identifying objects that are no longer reachable from any live code and reclaiming their memory. You never call free() in Java — the GC handles it. Modern JVMs use generational garbage collection based on one key observation: most objects die young. The heap is split into: - Young Generation — Where new objects are allocated. GC here is fast and frequent. - Old Generation — Objects that survive multiple young GC cycles get promoted here. GC here is slower but rare. - Metaspace — Class metadata (not objects). Managed separately. Different GC algorithms trade off throughput vs latency: - G1 GC (default since Java 9) — Balanced throughput and pause times, good for most applications - ZGC — Sub-millisecond pause times, designed for latency-sensitive systems - Parallel GC — Maximum throughput, accepts longer pauses, good for batch processing

The Full Pipeline — End to End

Here's the complete journey from source code to running program in one view: 1. You write HelloWorld.java — plain text source code 2. javac compiles it to HelloWorld.class — platform-neutral bytecode 3. You run java HelloWorld — JVM starts up 4. Class Loader finds and loads HelloWorld.class into memory 5. Bytecode Verifier checks the bytecode is safe and valid 6. Execution Engine begins interpreting bytecode immediately 7. JIT Compiler identifies hot methods and compiles them to native code 8. Garbage Collector runs in the background reclaiming unused memory 9. Your program runs — identically — on any platform with a JVM Each stage has a specific job. The compiler catches type errors. The verifier catches security violations. The interpreter handles startup. The JIT handles performance. The GC handles memory. Together, they make Java one of the most battle-tested runtime environments ever built.