Java remains one of the world's most popular languages nearly three decades after its creation. Understanding what happens between writing code and it executing on hardware explains both its portability and its performance characteristics.
The 4-Stage Lifecycle
1. Write source code — human-readable .java files
2. Compile with javac — produces platform-independent bytecode (.class files)
3. JVM loads and verifies bytecode
4. JVM executes — converting bytecode to machine instructions at runtime
The key insight: bytecode is platform-independent. The same .class file runs on Windows, macOS, and Linux. The JVM handles the platform-specific translation. That's how Java achieves "Write Once, Run Anywhere."
Stage 1: Compilation
When you run javac ProgramName.java, the compiler validates syntax, type-checks your code, and produces bytecode. If there are errors, compilation stops. If it succeeds, you get a .class file containing instructions the JVM understands — but no operating system does natively.
Stage 2: Class Loading
The JVM loads classes on demand, not all at once. Three class loaders handle this:
For each class, loading performs three functions: creates a binary stream from the class file, parses binary data into internal data structures, and creates a java.lang.Class instance.
Stage 3: Linking
Linking prepares a loaded class for execution through three steps:
Verification checks that the bytecode is well-formed and that code obeys Java language semantics — valid operation codes, correct method signatures, no security violations. This is the JVM's security boundary.
Preparation creates static fields and initializes them to default values. Memory is allocated but not yet populated with user-defined values.
Resolution checks symbolic references from one class to other classes and interfaces, loading them if necessary and verifying the references are correct.
Stage 4: Initialization
Initialization assigns values to static fields and executes static blocks:
class Config {
static int maxConnections = 100;
static String dbUrl;
static {
dbUrl = System.getenv("DATABASE_URL");
}
}Superclasses are always initialized before subclasses. A class initializes the first time it's actively used — instance creation, static method call, static field access (excluding constants).
Garbage Collection
Java manages memory automatically. The GC identifies objects that are no longer reachable (no live references in the call stack or static fields) and reclaims their memory.
You can suggest a GC run with System.gc(), but you cannot force it. The JVM decides when to collect based on heap pressure and its own heuristics.
The practical implication: prefer short-lived objects that die in the young generation (cheap to collect) over long-lived objects that get promoted to the old generation (more expensive to collect).
Class Unloading
Classes are only unloaded when their defining class loader becomes unreachable. Bootstrap-loaded classes are never unloaded. This matters primarily in application servers and plugin systems that load many classes dynamically — a memory leak in class loading can exhaust PermGen/Metaspace.
Why This Matters for Backend Engineers
Understanding the lifecycle helps you debug real problems:
The JVM does substantial work on your behalf. Understanding it makes you better at debugging, profiling, and writing code that works with — not against — the runtime.