The JVM's architecture and runtime memory areas — heap, stack, metaspace, PC register — which error each one throws when exhausted, classloader delegation, escape analysis, JIT tiers, and reference strength.
Published September 23, 2026
The JVM has four cooperating subsystems: the Class Loader Subsystem loads .class files into memory; the Runtime Data Areas (heap, stacks, metaspace — detailed below) hold the actual data; the Execution Engine (interpreter + JIT compiler) runs bytecode; and the Native Interface (JNI) bridges to native libraries when Java code calls into C/C++.
new allocation (that doesn't get optimized away — see escape analysis below) lands here.-Xmx, only by available system memory (or an explicit -XX:MaxMetaspaceSize).These are two of the most conflated errors in Java, and the distinction is entirely about which memory area is exhausted:
StackOverflowError: the per-thread stack ran out of space — almost always uncontrolled recursion with no base case, or a base case that's never reached.OutOfMemoryError: Java heap space: the heap is full and GC can't reclaim enough to satisfy an allocation — too many live objects, or a memory leak (see Memory Leaks in Java).OutOfMemoryError: Metaspace: class metadata exhausted metaspace — commonly caused by dynamically generating and loading huge numbers of classes at runtime (some bytecode-generation-heavy frameworks, or a classloader leak — see below).Bootstrap ClassLoader (native code, loads core JDK classes: java.lang.*, etc.)
↑ parent
Platform/Extension ClassLoader (loads JDK extension classes)
↑ parent
Application ClassLoader (loads YOUR classpath)
When a class needs loading, the request delegates upward first — the Application classloader asks its parent (Platform) before trying itself, which asks its parent (Bootstrap) before trying itself. This parent-first order is a deliberate security/consistency measure: it prevents your own application code from shadowing a core JDK class (like java.lang.String) with a malicious or accidental same-named class of your own — the Bootstrap loader always gets first crack at core class names.
A JIT optimization that determines whether an object's reference ever "escapes" the method (or thread) that created it — is it returned, stored in a field, passed to another method that might retain it? If the JIT proves an object never escapes, it can allocate it on the stack instead of the heap (cheaper, automatically reclaimed when the frame pops, no GC involvement at all) or even eliminate the allocation entirely via scalar replacement (breaking the object into its individual fields as local variables, with no object ever actually materializing). This is why microbenchmarks that create short-lived, clearly-local objects in a tight loop often show far less GC pressure than a naive read of the code would suggest — escape analysis is quietly doing this optimization for you.
The execution engine starts by interpreting bytecode directly (slow per-instruction, but zero compilation delay) and profiles which methods run frequently ("hot" methods). Hot methods get compiled to optimized native machine code at runtime — tiered compilation uses a fast-but-less-optimizing compiler first (C1) for a quick win, then a slower-but-more-aggressively-optimizing compiler (C2) for methods that stay hot long enough to justify the extra compilation cost. This balances startup speed (interpreting immediately, no compile-time wait) against peak throughput (heavily-optimized native code for the methods that actually matter to overall performance).
Beyond a normal ("strong") reference, the JDK provides three progressively weaker reference types, each solving a different memory-sensitive use case:
SoftReference — cleared only under memory pressure (right before the JVM would otherwise throw OutOfMemoryError). Good for caches: keep data around as long as there's spare memory, but let the GC reclaim it before crashing the application.WeakReference — cleared on the next GC cycle, regardless of memory pressure. Good for canonicalizing maps (e.g. WeakHashMap) where you want an entry to disappear automatically the moment nothing else references its key, without needing to explicitly remove it — directly relevant to the Observer Pattern's listener-leak problem, where weak references are a common fix for un-removed observers.PhantomReference — enqueued only after the object has already been finalized, and get() always returns null — you can never actually access the object through a phantom reference. Used purely for cleanup tracking (scheduling post-mortem resource cleanup), a safer, more deterministic alternative to the deprecated finalize() mechanism.Q: Why is metaspace allocated natively instead of on the heap, unlike its PermGen predecessor?
A: PermGen had a fixed size that was notoriously hard to tune correctly (undersized PermGen was a common source of OutOfMemoryError: PermGen space in pre-Java-8 applications with lots of dynamically generated classes) — moving metadata to native memory removed that fixed ceiling, letting it grow with available system memory instead of a pre-allocated heap region.
Q: If escape analysis can eliminate allocations, why doesn't every object get stack-allocated? A: Escape analysis has to prove an object never escapes, which is only possible for objects with fully-analyzable lifetimes within the JIT's optimization scope (typically requires the object's usage to be fully visible within the compiled method, often after inlining) — any object stored in a field, returned, passed somewhere the JIT can't fully trace, or created via reflection, defeats the analysis and falls back to normal heap allocation.
Q: How does classloader parent-first delegation interact with a 'classloader leak' in a redeployed web app?
A: A classloader leak happens when the application classloader itself (not its parent) can't be garbage collected after a redeploy, because something (a thread, a static field in a framework the app uses, a JDBC driver registered in a shared registry) still holds a reference into the old classloader's loaded classes — every redeploy then leaks the entire old classloader and every class/object graph it loaded, which is why long-running app servers with frequent redeploys are the classic environment where this surfaces as slow, cumulative OutOfMemoryError: Metaspace.
Q: When would you deliberately choose SoftReference over just using a fixed-size LRU cache (like the LinkedHashMap-based one from TreeMap & LinkedHashMap)? A: An LRU cache gives you deterministic, bounded memory use with a size you control explicitly — a SoftReference-based cache instead lets the JVM decide dynamically how much to retain based on actual memory pressure, which can use available memory more fully when it's plentiful but offers no predictable bound, making it a better fit when cache size should flex with available headroom rather than being fixed in advance.