JVM Architecture
ClassLoader, Runtime Data Areas, Heap, Stack and Metaspace
JVM Architecture
The runtime engine behind Java's "Write Once, Run Anywhere" promise â from class loading through bytecode execution to memory management.
1. Definition
What is it?
The Java Virtual Machine (JVM) is an abstract computing machine that provides a runtime environment for executing Java bytecode. It acts as the intermediary layer between compiled Java programs (.class files) and the underlying operating system and hardware.
Why does it exist?
The JVM exists to fulfil Java's Write Once, Run Anywhere (WORA) principle. A Java program is compiled once into platform-neutral bytecode; the JVM on each target platform interprets or further compiles that bytecode into native machine instructions. This removes the need to recompile source code per platform.
Where does it fit?
Execution flow at a high level:
- Java source (
.java) is compiled byjavac. - The result is bytecode (
.class). - The JVM on each platform executes or further compiles that bytecode.
- The instructions ultimately run on the native OS / CPU.
The JVM sits between the bytecode and the hardware. The JDK contains the compiler (javac) and the JVM; the JRE contains only the JVM and standard libraries.
2. Core Concepts
2.1 JVM Architecture Overview
Main JVM building blocks:
- ClassLoader subsystem â loads classes through Bootstrap, Platform/Extension, and Application loaders.
- Runtime data areas â heap, metaspace, Java stacks, PC registers, and native stacks.
- Execution engine â interpreter, JIT compiler, and garbage collector.
- JNI â bridge to native methods.
2.2 ClassLoader Subsystem
The ClassLoader subsystem is responsible for loading, linking, and initialising classes at runtime.
Classloader hierarchy
| ClassLoader | Loads | Parent |
|---|---|---|
| Bootstrap | java.lang.*, java.util.*, core JDK classes (from jrt:/) |
none (native) |
| Platform (formerly Extension) | JDK extension modules (java.se, jdk.*) |
Bootstrap |
| Application (System) | Classes on the application classpath (-cp) |
Platform |
| Custom | User-defined class sources (jars, network, DBâŠ) | Any of the above |
Delegation model (Parent-First)
Parent-first delegation in practice:
- Application ClassLoader first asks its parent to load
com.example.Foo. - Platform ClassLoader forwards the request upward.
- Bootstrap ClassLoader tries first and, if not found, returns control downward.
- The class is finally loaded by the first classloader that can resolve it, often the Application ClassLoader.
Each classloader first delegates to its parent before attempting to load the class itself. This prevents user code from accidentally (or maliciously) replacing core JDK classes.
Class loading phases
| Phase | Description |
|---|---|
| Loading | Reads the .class binary and creates a Class object |
| Verification | Checks bytecode correctness and security constraints |
| Preparation | Allocates memory for static fields; assigns default values |
| Resolution | Replaces symbolic references with direct memory references |
| Initialization | Executes <clinit> (static initialisers and static blocks) |
2.3 Runtime Data Areas
Heap (shared across all threads)
The heap is the primary memory area for object allocation and is managed by the Garbage Collector.
Heap regions at a glance:
Young Generation â where most new objects begin.
Eden Space â fresh allocations start here.
Survivor 0 / Survivor 1 â hold objects that survived one or more Minor GCs.
Old (Tenured) Generation â stores long-lived objects promoted out of the young generation.
Objects are first allocated in Eden.
After a Minor GC, surviving objects move to a Survivor space; after enough survivals they are promoted to Old Gen.
A Major/Full GC collects the entire heap.
Metaspace (Method Area â since Java 8)
Stores class metadata: class structures, method bytecode, constant pool, field/method descriptors, static variables.
- Before Java 8: stored in PermGen (fixed, on-heap).
- Since Java 8: stored in Metaspace (native/off-heap memory â grows dynamically by default).
| PermGen (†Java 7) | Metaspace (℠Java 8) | |
|---|---|---|
| Location | Java heap | Native memory |
| Default size | Fixed (~64â256 MB) | Unbounded (system RAM) |
| OOM cause | Too many classes | Unbounded native growth |
| Control flag | -XX:MaxPermSize |
-XX:MaxMetaspaceSize |
Java Stacks (per-thread)
Each thread has its own Java Stack, which stores stack frames. A new frame is pushed for each method call and popped when the method returns.
A stack frame contains:
- Local Variable Array â method arguments and local variables
- Operand Stack â working area for bytecode instructions
- Frame Data â reference to the constant pool, return value info
Example stack state:
- Thread-1 stack may contain frames for
main()âfoo()âbar(). - Thread-2 stack may simultaneously contain an unrelated frame such as
run(). - Each thread owns its own stack; stack frames are never shared across threads.
Default stack size is ~512 KBâ1 MB per thread. Exceeded by deep/infinite recursion â StackOverflowError.
PC (Program Counter) Registers (per-thread)
Holds the address of the currently executing bytecode instruction for each thread. Undefined for native methods.
Native Method Stacks (per-thread)
Supports execution of native (C/C++) methods called via JNI. Separate from Java stacks.
2.4 Execution Engine
| Component | Role |
|---|---|
| Interpreter | Executes bytecode instructions one at a time; fast startup, slow steady-state |
| JIT Compiler | Detects "hot" methods (via profiling) and compiles them to native code; slow warm-up, fast execution |
| C1 Compiler | Fast, lightly optimised JIT tier (client compiler) |
| C2 Compiler | Heavily optimised JIT tier (server compiler); used for hot paths |
| Garbage Collector | Reclaims unreachable heap objects (G1, ZGC, Shenandoah, ParallelGCâŠ) |
Modern JVMs use tiered compilation: code starts interpreted (Tier 0), then moves through C1 (Tiers 1â3) and finally C2 (Tier 4) as it gets hotter.
3. Practical Usage
When to configure JVM memory settings
- Production deployments â always set
-Xms(initial heap) equal to-Xmx(max heap) to prevent heap resizing pauses. - Containerised environments â use
-XX:+UseContainerSupport(default since Java 10) so the JVM reads container memory limits instead of host RAM. - Many dynamically loaded classes (frameworks, OSGi, app servers) â set
-XX:MaxMetaspaceSizeto prevent runaway native memory growth. - High thread count applications â reduce
-Xss(stack size) to fit more threads in RAM; increase it for deeply recursive algorithms.
When NOT to over-tune
- Development/test â default settings are fine; premature tuning wastes time.
- Opinionated runtimes (Spring Boot native, GraalVM native-image) â JVM flags don't apply; use their own configuration.
Key JVM flags reference
| Flag | Purpose | Example |
|---|---|---|
-Xms |
Initial heap size | -Xms512m |
-Xmx |
Maximum heap size | -Xmx2g |
-Xss |
Thread stack size | -Xss256k |
-XX:MaxMetaspaceSize |
Cap Metaspace | -XX:MaxMetaspaceSize=256m |
-XX:+PrintGCDetails |
Verbose GC logging | â |
-XX:+UseG1GC |
Select G1 collector | â |
-XX:+HeapDumpOnOutOfMemoryError |
Dump heap on OOM | â |
4. Code Examples
Basic Example â Inspecting ClassLoaders
public class ClassLoaderInspector {
public static void main(String[] args) {
// Application classloader
ClassLoader appCL = ClassLoaderInspector.class.getClassLoader();
System.out.println("App CL: " + appCL);
// Platform (extension) classloader
ClassLoader platformCL = appCL.getParent();
System.out.println("Platform CL: " + platformCL);
// Bootstrap classloader â represented as null in Java
ClassLoader bootstrapCL = platformCL.getParent();
System.out.println("Bootstrap CL: " + bootstrapCL); // null
// Core class loaded by bootstrap
ClassLoader stringCL = String.class.getClassLoader();
System.out.println("String's CL: " + stringCL); // null (bootstrap)
}
}
Expected output (Java 17+):
App CL: jdk.internal.loader.ClassLoaders$AppClassLoader@...
Platform CL: jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
Bootstrap CL: null
String's CL: null
Advanced Example â Stack Frame and StackOverflowError
public class StackDemo {
// Each call pushes a new frame onto the thread's Java Stack
static int countDown(int n) {
if (n == 0) return 0;
return 1 + countDown(n - 1); // recursive call = new stack frame
}
// Infinite recursion â StackOverflowError
static void infinite() {
infinite();
}
public static void main(String[] args) {
System.out.println(countDown(10)); // prints 10, fine
try {
infinite();
} catch (StackOverflowError e) {
// StackOverflowError is an Error, not Exception
System.out.println("Stack overflow caught: " + e);
}
}
}
Note:
StackOverflowErrorextendsError, notException. It is recoverable in acatchblock, but the thread's stack is in an unpredictable state â do not continue using that thread for meaningful work after catching it.
Metaspace Settings Example
# Run with a capped Metaspace to avoid unbounded native memory growth
java -XX:MaxMetaspaceSize=128m \
-XX:MetaspaceSize=64m \
-XX:+PrintGCDetails \
-jar myapp.jar
To diagnose Metaspace:
# Print native memory usage breakdown (requires -XX:NativeMemoryTracking=summary)
java -XX:NativeMemoryTracking=summary -jar myapp.jar &
jcmd <PID> VM.native_memory summary
Common Pitfall â OutOfMemoryError scenarios
import java.util.ArrayList;
import java.util.List;
public class OOMDemo {
// 1. Heap OOM â retain objects so GC cannot collect them
static void heapOOM() {
List<byte[]> leak = new ArrayList<>();
while (true) {
leak.add(new byte[1024 * 1024]); // 1 MB chunks
}
// throws java.lang.OutOfMemoryError: Java heap space
}
// 2. Metaspace OOM â generate classes at runtime (e.g. via proxies/ASM)
// Not easily reproducible inline; use frameworks that generate many classes
// throws java.lang.OutOfMemoryError: Metaspace
// 3. Stack OOM (StackOverflowError)
static void stackOOM() {
stackOOM(); // unbounded recursion
// throws java.lang.StackOverflowError
}
}
5. Trade-offs
| Aspect | Details |
|---|---|
| ⥠Performance | Larger heap â fewer GC pauses but longer pause duration when GC does run; smaller heap â more frequent but shorter pauses |
| đŸ Memory | Metaspace uses native memory outside the heap; MaxMetaspaceSize must be set explicitly or it can exhaust OS memory |
| đ§ Maintainability | Over-tuned JVM flags make configuration fragile across JVM versions and deployments |
| đ Flexibility | JIT tiered compilation adapts at runtime; warm-up time (~thousands of invocations) must be accounted for in benchmarks |
| đ Startup | JIT warm-up adds latency â problematic for serverless/short-lived processes; GraalVM native-image offers AOT as an alternative |
| đ§” Concurrency | Heap is shared â object allocation is thread-safe via TLAB; stacks are per-thread â no sharing, no synchronisation needed |
6. Common Mistakes
1. â Not setting `-Xms` equal to `-Xmx`
# Bad â JVM starts small and wastes time resizing the heap
java -Xmx4g -jar app.jar
# Good â pre-allocate the full heap upfront
java -Xms4g -Xmx4g -jar app.jar
2. â Ignoring Metaspace limits
# Bad â Metaspace grows until the OS runs out of memory
java -jar app.jar
# Good â cap Metaspace
java -XX:MaxMetaspaceSize=256m -jar app.jar
3. â Catching StackOverflowError and continuing
// Bad â stack state is corrupted after overflow
void process() {
try {
recursiveOp();
} catch (StackOverflowError e) {
process(); // calling more code on a possibly broken stack
}
}
// Good â log, abort gracefully, do not recurse further
void process() {
try {
recursiveOp();
} catch (StackOverflowError e) {
log.error("Stack overflow in process()", e);
throw new RuntimeException("Processing failed due to recursion depth", e);
}
}
4. â Confusing heap and stack storage
// Stack stores: primitive local variables, object references
// Heap stores: the actual object instances
void example() {
int x = 42; // x is on the STACK
String s = "hello"; // reference 's' is on the STACK; the String object is on the HEAP
Object obj = new Object(); // same â reference on stack, object on heap
}
5. â Assuming class loading is eager
// Classes are loaded lazily â only when first referenced
// This means static initialisers run at first use, not at startup
class Config {
static {
System.out.println("Config loaded"); // printed only on first use of Config
}
}
7. Senior-level Insights
Custom ClassLoaders
Custom classloaders are the foundation of plugin systems, hot-reload, OSGi, and application server isolation. Each webapp in Tomcat runs in its own WebAppClassLoader so classes from different apps don't collide.
public class PluginClassLoader extends URLClassLoader {
public PluginClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// Child-first: try loading ourselves before delegating to parent
// Useful for plugin isolation
synchronized (getClassLoadingLock(name)) {
Class<?> loaded = findLoadedClass(name);
if (loaded != null) return loaded;
try {
return findClass(name); // try own classpath first
} catch (ClassNotFoundException e) {
return super.loadClass(name, resolve); // fall back to parent
}
}
}
}
Escape Analysis and Stack Allocation
The JIT compiler performs escape analysis: if an object does not "escape" the method (no external references), the JVM may allocate it on the stack instead of the heap â avoiding GC pressure entirely.
// The Point object may be stack-allocated by C2 if escape analysis proves
// it doesn't escape the method
double distanceFromOrigin(double x, double y) {
Point p = new Point(x, y); // may never reach the heap
return Math.sqrt(p.x * p.x + p.y * p.y);
}
Use -XX:+PrintEscapeAnalysis (debug JVM builds) or -XX:+EliminateAllocations (enabled by default in C2) to observe this.
TLAB (Thread Local Allocation Buffer)
To avoid synchronisation on every new operation, the JVM pre-allocates a private chunk of Eden space per thread â the TLAB. Object allocation within a TLAB is a simple pointer bump â effectively free.
TLAB in practice:
- Eden space can be split into thread-local allocation buffers.
- Thread-1 and Thread-2 can each allocate from their own private chunk without locking.
- Allocation is typically just a pointer bump, which makes object creation very cheap.
Tune with -XX:TLABSize if profiling shows excessive TLAB refills.
JVM Tuning in Containerised Environments
Before Java 10, the JVM read host memory for ergonomic defaults, causing it to size heaps and thread pools far beyond container limits. Always verify:
java -XX:+PrintFlagsFinal -version | grep -E "MaxHeapSize|ActiveProcessor"
Useful Diagnostic Commands
# List all JVM processes
jps -l
# Heap histogram (top object types by count/size)
jmap -histo <PID>
# Thread dump (detect deadlocks, blocked threads)
jstack <PID>
# Real-time JVM stats
jstat -gcutil <PID> 1000 # GC stats every 1 second
8. Glossary
| Term | Definition |
|---|---|
| JVM | Java Virtual Machine â abstract machine that executes Java bytecode |
| Bytecode | Platform-neutral instruction set compiled from Java source; stored in .class files |
| ClassLoader | Component responsible for loading class definitions into the JVM at runtime |
| Heap | Shared memory area for object instances, managed by the Garbage Collector |
| Stack Frame | Per-method-call data structure on the Java Stack; holds locals, operand stack, and return info |
| Metaspace | Native memory area (since Java 8) storing class metadata; replaced PermGen |
| PermGen | Permanent Generation â fixed heap area for class metadata in Java †7; removed in Java 8 |
| JIT | Just-In-Time compiler â compiles hot bytecode paths to native machine code at runtime |
| TLAB | Thread Local Allocation Buffer â per-thread Eden chunk for fast, lock-free object allocation |
| Escape Analysis | JIT optimisation that detects whether objects can be stack-allocated or scalar-replaced |
| Minor GC | Garbage collection of the Young Generation only |
| Major/Full GC | Garbage collection of the entire heap (Young + Old Generation) |
9. Cheatsheet
- đïž JVM = ClassLoader + Runtime Data Areas + Execution Engine
- đŠ ClassLoaders: Bootstrap â Platform â Application (parent-first delegation)
- đ Heap is shared across all threads; managed by GC
- đ§” Stack is per-thread; each method call = one new stack frame
- đïž Metaspace (Java 8+) stores class metadata in native memory â set
-XX:MaxMetaspaceSize - âïž JIT compiles hot code to native; warm-up takes thousands of invocations
- đš
StackOverflowError= stack frames exhausted (deep/infinite recursion) - đš
OutOfMemoryError: Java heap space= heap full, GC cannot free enough - đš
OutOfMemoryError: Metaspace= too many loaded classes, cap with-XX:MaxMetaspaceSize - ⥠TLAB makes
newnearly free â pointer bump inside a thread-local Eden chunk
đź Games
8 questions