Intermediate Reading time: ~14 min

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:

  1. Java source (.java) is compiled by javac.
  2. The result is bytecode (.class).
  3. The JVM on each platform executes or further compiles that bytecode.
  4. 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:

  1. Application ClassLoader first asks its parent to load com.example.Foo.
  2. Platform ClassLoader forwards the request upward.
  3. Bootstrap ClassLoader tries first and, if not found, returns control downward.
  4. 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:MaxMetaspaceSize to 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: StackOverflowError extends Error, not Exception. It is recoverable in a catch block, 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 new nearly free — pointer bump inside a thread-local Eden chunk

🎼 Games

8 questions