Why this matters
ClassNotFoundException,NoClassDefFoundErrorandOutOfMemoryErrorare 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
What each part does
- Class loader subsystem finds a
.classfile, 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
Classobject. 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.
Nothing is found on the way up? Control walks back down and the first loader that can read the file defines the class.
The three phases are observable. Static fields exist with default values before initialisation assigns real ones:
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");
}
}
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
Popped the moment a method returns.
Cleared by the garbage collector, not by returning.
| Aspect | Stack | Heap |
|---|---|---|
| Holds | Method frames, local variables, references | All objects and their instance fields |
| Shared | One private stack per thread | Shared by every thread |
| Lifetime | A frame dies when the method returns | Until no reference remains |
| Reclaimed by | Automatically, on return | The garbage collector |
| Error when full | StackOverflowError | OutOfMemoryError |
Holds
StackMethod frames, local variables, referencesHeapAll objects and their instance fieldsShared
StackOne private stack per threadHeapShared by every threadLifetime
StackA frame dies when the method returnsHeapUntil no reference remainsReclaimed by
StackAutomatically, on returnHeapThe garbage collectorError when full
StackStackOverflowErrorHeapOutOfMemoryError
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.
StackOverflowErrormeans deep recursion;OutOfMemoryErrormeans 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.