PrepZone Logo
PrepZone

Inside the JVM

Class loading, the runtime data areas, and how the interpreter and JIT compiler split the work.

Read these first

Why this matters

  • ClassNotFoundException, NoClassDefFoundError and OutOfMemoryError are all symptoms you can diagnose instantly once you know which subsystem owns which failure.
  • Garbage collection, memory tuning and performance profiling all assume this layout. Learning it now makes those topics descriptive rather than mysterious.
  • "Explain the JVM architecture" is a standing interview question, and the structured answer below is the one that lands.

The three subsystems

1. Class loader subsystem
LoadFind and read .class
LinkVerify, prepare, resolve
InitialiseRun static setup
2. Runtime data areas
Method areaClass metadata
HeapAll objects
StacksOne per thread
3. Execution engine
InterpreterStarts instantly
JIT compilerOptimises hot code
Garbage collectorFrees unused objects
Loading brings classes in, the memory areas hold their state, and the execution engine turns bytecode into instructions the CPU understands.

What each part does

  • Class loader subsystem finds a .class file, reads it into memory, checks it is valid, prepares its static fields and resolves its references to other classes.
  • Runtime data areas are the memory regions: the heap for objects, one stack per thread for method calls, metaspace for class definitions, plus small per-thread bookkeeping registers.
  • Execution engine runs the bytecode. It contains an interpreter for immediate execution, a JIT compiler for frequently used code, and the garbage collector that reclaims unreachable objects.

Loading: the three phases

Class loading is often summarised as "it loads the class", but it has distinct stages, and errors surface in specific ones.

  • Loading reads the bytes and creates a Class object. Three loaders cooperate in a chain: the bootstrap loader handles core JDK classes, the platform loader handles additional JDK modules, and the application loader handles your classpath.
  • Linking has three steps of its own. Verification rejects malformed or unsafe bytecode. Preparation allocates static fields and sets them to default values — 0, false, null. Resolution turns symbolic references to other classes into direct ones.
  • Initialisation runs static initialisers and static blocks, in the order they appear in the source. This is the first point at which your code executes.
Application loaderYour classpath — asks its parent first
Platform loaderExtra JDK modules
Bootstrap loaderjava.lang, java.util — loads first

Nothing is found on the way up? Control walks back down and the first loader that can read the file defines the class.

A request travels up to the most trusted loader first. Only if no parent can find the class does the child load it, so core JDK classes can never be replaced by application code.

The three phases are observable. Static fields exist with default values before initialisation assigns real ones:

Java
public class LoadOrder {
    static int counter;              // preparation sets this to 0
    static String name = compute();  // initialisation assigns this

    static {
        System.out.println("static block, counter = " + counter);
    }

    static String compute() {
        System.out.println("static field initialiser");
        return "ready";
    }

    public static void main(String[] args) {
        System.out.println("main");
    }
}
Java
static field initialiser
static block, counter = 0
main

The field initialiser and the static block run in source order, both before main. The counter is already 0 rather than unset, because preparation happened earlier.

Memory: who gets what

Stack — per thread
main() frameuser → reference
greet() framecount = 3 (primitive)

Popped the moment a method returns.

Heap — shared
User objectname = "Asha"
String object"Asha"

Cleared by the garbage collector, not by returning.

The variable sits in the thread's stack frame. The object it names lives in the shared heap, which is why two variables can point at one object.
AspectStackHeap
HoldsMethod frames, local variables, referencesAll objects and their instance fields
SharedOne private stack per threadShared by every thread
LifetimeA frame dies when the method returnsUntil no reference remains
Reclaimed byAutomatically, on returnThe garbage collector
Error when fullStackOverflowErrorOutOfMemoryError
  • Holds

    StackMethod frames, local variables, references
    HeapAll objects and their instance fields
  • Shared

    StackOne private stack per thread
    HeapShared by every thread
  • Lifetime

    StackA frame dies when the method returns
    HeapUntil no reference remains
  • Reclaimed by

    StackAutomatically, on return
    HeapThe garbage collector
  • Error when full

    StackStackOverflowError
    HeapOutOfMemoryError

Deep recursion fills a stack. Too many live objects fill the heap. The error tells you which.

Class metadata — field names, method bytecode, the constant pool — lives in metaspace, which sits outside the heap in native memory. Before Java 8 this was a fixed-size region called PermGen, and overflowing it was a classic failure when redeploying an application server repeatedly. Metaspace grows on demand, which made that error much rarer.

Execution: interpret first, compile later

The execution engine deliberately does both.

  • The interpreter reads one bytecode instruction at a time and performs it. Starting is instant, but repeated work is repeated from scratch.
  • A counter tracks invocations and loop iterations per method. Once a threshold is crossed the method is hot.
  • The JIT compiler compiles hot methods to native code and caches the result in the code cache. Later calls jump straight to the compiled version.
  • Because compilation happens with runtime information available, the JIT can inline small methods, drop branches that never execute, and devirtualise calls whose real type is always the same.

HotSpot actually runs two compilers in tiers. C1 compiles quickly with modest optimisation to get past interpretation sooner; C2 compiles slowly with aggressive optimisation for the hottest code. This is why a server's response times improve over its first few minutes and then settle — the phenomenon known as warm-up.

Common misreadings

  • "The class loader runs static blocks when it loads the class." Loading and initialisation are separate. Static blocks run at initialisation, which may be much later — triggered by the first instantiation or static access.
  • "ClassNotFoundException and NoClassDefFoundError are the same." The exception means a lookup by name failed at run time, usually from Class.forName. The error means the class was present at compile time but is missing now, which is almost always a packaging problem.
  • "Each thread gets its own heap." Only stacks are per-thread. One shared heap is exactly why thread safety is a concern at all.
  • "The JIT compiles everything." Compiling is expensive, so cold code stays interpreted forever. That is the correct trade-off.

Quick recall

Everything you need if you only revisit this box.

  • Three subsystems: class loader, runtime data areas, execution engine.
  • Class loading runs loading → linking (verify, prepare, resolve) → initialisation. Static blocks run last, not first.
  • Loaders delegate to their parent first, which is why core JDK classes cannot be overridden.
  • Stack is per-thread and holds frames and references; heap is shared and holds objects; metaspace holds class metadata in native memory.
  • StackOverflowError means deep recursion; OutOfMemoryError means too many live objects.
  • The engine interprets first, then the JIT compiles hot methods to cached native code — the source of JVM warm-up.

Test yourself

Answer these before moving on — recall is what makes it stick.