Knowing how the JVM manages memory explains everyday problems: OutOfMemoryError in a long test run, CI agents slowing down, and why == on strings sometimes works. This guide covers the memory areas, garbage collection and collectors, the JVM flags worth knowing, and the ClassLoader hierarchy. For how code gets compiled and started, see How Java Code Runs.

JVM Memory Model

JVM Memory Areas

JVM Memory Structure:

┌─────────────────────────────────────────────────────┐
│                    METHOD AREA                       │
│  (class metadata, static vars, constant pool)        │
├─────────────────────────────────────────────────────┤
│                       HEAP                           │
│  ┌────────────────┐  ┌──────────────────────────┐   │
│  │  Young Gen     │  │      Old Gen             │   │
│  │ ┌────┬───────┐ │  │  (long-lived objects)    │   │
│  │ │Eden│Surv S0│ │  │                          │   │
│  │ │    │Surv S1│ │  └──────────────────────────┘   │
│  │ └────┴───────┘ │                                  │
│  └────────────────┘                                  │
├─────────────────────────────────────────────────────┤
│              THREAD-SPECIFIC (per thread)             │
│  Stack (frames, locals, ops) | PC Register | Native  │
└─────────────────────────────────────────────────────┘
// HEAP: shared across all threads
// - Young Gen: new objects (Eden + 2 Survivor spaces)
// - Old Gen (Tenured): survived multiple GC cycles
// - Metaspace (Java 8+): class metadata (no fixed limit!)

// STACK: one per thread
// - Stores stack frames (one per method call)
// - Each frame: local variables, operand stack, reference to CP
// - StackOverflowError: too many nested calls

// METHOD AREA (Metaspace): shared
// - Class info, method bytecode, static variables
// - String constant pool

// Garbage Collection flow:
// 1. New object → Eden
// 2. Minor GC: Eden + S0 → S1 (survivors age++)
// 3. After N GC cycles (N=15 default): Old Gen
// 4. Major/Full GC: Old Gen collected (slower, "Stop The World")

// GC Algorithms:
// Serial GC       — single-threaded, small apps
// Parallel GC     — multi-threaded minor GC, throughput focus
// G1 GC (default) — region-based, balanced throughput+latency
// ZGC/Shenandoah  — ultra-low pause (<10ms), Java 11/12+

Interview Questions

What is the difference between Stack and Heap?

Stack: per-thread, stores local primitives and references, LIFO, fast, fixed size, auto-managed. Heap: shared, stores all objects, managed by GC, dynamic size. Stack overflow = too deep recursion. Heap overflow = OutOfMemoryError.

What triggers Garbage Collection?

GC is triggered when Eden space is full (Minor GC) or Old Gen is full (Major GC). Cannot be forced — System.gc() is just a hint. Objects are eligible for GC when unreachable from any GC root (local variables, static fields, active thread stacks).

Advertisement

JVM Tuning & Garbage Collection

// ── KEY JVM FLAGS ──

// Heap size
// -Xms512m          initial heap size (512 MB)
// -Xmx4g            max heap size (4 GB)
// -Xss256k          thread stack size

// GC algorithm selection
// -XX:+UseG1GC              G1 (default Java 9+, balanced)
// -XX:+UseZGC               ZGC (Java 15+, low latency <1ms pauses)
// -XX:+UseShenandoahGC      Shenandoah (ultra-low latency)
// -XX:+UseParallelGC        Parallel (throughput focus)
// -XX:+UseSerialGC          Serial (small single-core apps)

// G1 GC tuning
// -XX:MaxGCPauseMillis=200  target max pause (ms)
// -XX:G1HeapRegionSize=16m  region size (1-32 MB)
// -XX:InitiatingHeapOccupancyPercent=45  when to start concurrent GC

// Logging
// -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20m

// Diagnostics
// -XX:+HeapDumpOnOutOfMemoryError  dump heap on OOME
// -XX:HeapDumpPath=/tmp/heapdump.hprof
// -XX:+PrintFlagsFinal   print all JVM flag values

// ── GC GENERATIONS ──
// Young Gen: Eden + Survivor 0 + Survivor 1
//   Minor GC: triggered when Eden full (fast, ~ms)
//   Objects surviving N GCs (default 15) promoted to Old Gen

// Old Gen (Tenured):
//   Major/Full GC: triggered when Old Gen fills (slow, Stop-The-World)

// Metaspace (Java 8+): class metadata, no fixed limit
//   -XX:MaxMetaspaceSize=256m  to cap it

// ── VIEWING GC ACTIVITY ──
// VisualVM, JConsole: GUI tools
// jstat -gcutil <pid> 1000  command line, every 1 second
// jmap -heap <pid>  heap summary
// jmap -dump:format=b,file=heap.hprof <pid>  heap dump

ClassLoader Hierarchy

// THREE built-in ClassLoaders:

// 1. Bootstrap ClassLoader
//    - Loads JDK core classes (java.lang, java.util, etc.)
//    - Part of JVM itself (written in C++)
//    - Parent of all ClassLoaders
//    - String.class.getClassLoader() → null (Bootstrap)

// 2. Extension (Platform) ClassLoader
//    - Loads classes from Java extensions ($JAVA_HOME/lib/ext)
//    - Parent: Bootstrap ClassLoader

// 3. Application (System) ClassLoader
//    - Loads your application classes (classpath)
//    - Parent: Extension ClassLoader

// ── DELEGATION MODEL ──
// When loading class X:
// 1. Application ClassLoader asks parent (Extension)
// 2. Extension asks parent (Bootstrap)
// 3. Bootstrap tries to load — success: done!
// 4. Bootstrap fails → Extension tries
// 5. Extension fails → Application tries
// 6. All fail → ClassNotFoundException
// Reason: prevents user code from overriding core classes

// ── CUSTOM CLASSLOADER ──
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classBytes = loadClassBytes(name); // load from custom source
        return defineClass(name, classBytes, 0, classBytes.length);
    }
    private byte[] loadClassBytes(String name) {
        // Could load from network, encrypted file, database, etc.
        String path = name.replace('.', '/') + ".class";
        try (InputStream is = getClass().getResourceAsStream(path)) {
            return is.readAllBytes();
        } catch (Exception e) { return new byte[0]; }
    }
}

// Usage: hot-reload classes without restarting JVM
// Used by: OSGi, Java EE app servers, plugin systems

JVM Memory Model — All Details

// ── OBJECT HEADER (every Java object has this) ──
// Mark Word (8 bytes on 64-bit):
//   Unlocked: hashcode(25) | age(4) | biased_lock(1) | lock(2=00)
//   Biased:   thread_id(54) | epoch(2) | age(4) | lock(3=101)
//   Thin lock: ptr_to_lock_record(62) | lock(2=00)
//   Fat lock:  ptr_to_monitor(62) | lock(2=10)
//   GC mark:   forwarding_pointer(62) | lock(2=11)
// Class Pointer (8 bytes, or 4 with compressed oops): points to class metadata
// Array Length (4 bytes, only for arrays)

// Total object overhead: 16 bytes minimum!
// new Object() = 16 bytes on heap

// ── COMPRESSED OOPS ──
// With -XX:+UseCompressedOops (default for heap < 32GB):
// Object references compressed from 8 bytes → 4 bytes
// Saves ~40% memory for reference-heavy apps
// Works by assuming 8-byte alignment (can address 32GB with 4-byte refs)

// ── TLAB (Thread Local Allocation Buffer) ──
// Each thread gets a private slice of Eden
// Object allocation = just bump a pointer (no synchronization!)
// When TLAB full: get new TLAB from Eden (brief sync)
// Default TLAB size: ~1% of Eden
// -XX:TLABSize=512k (customize)
// -XX:+PrintTLAB (debug)

// ── ESCAPE ANALYSIS ──
// JVM detects if object 'escapes' the method
// If object doesn't escape: allocate on STACK instead of heap!
// Result: zero GC pressure, blazing fast

public int compute() {
    Point p = new Point(1, 2); // may be stack-allocated!
    return p.x + p.y;          // object doesn't escape method
}
// JVM can also 'scalar replace': eliminate object entirely,
// use just its fields as local variables

// ── GC ROOTS ──
// Objects reachable from GC roots are NOT collected:
// 1. Local variables in all active threads' stack frames
// 2. Static variables of all loaded classes
// 3. JNI references
// 4. Synchronized monitor locks
// 5. JVM internal references (system class loader, etc.)

// ── REFERENCE TYPES ──
import java.lang.ref.*;

// Strong reference: default, GC never collects
Object strong = new Object();

// Soft reference: GC collects when memory low
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024*1024]);
byte[] data = softRef.get(); // null if GC'd
// Use for: image caches, data caches

// Weak reference: GC collects at next GC cycle
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get(); // null if GC'd
// Use for: canonicalization maps, WeakHashMap keys

// Phantom reference: no get()! notification AFTER GC
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);
// phantom.get() always returns null
// queue.poll() returns ref AFTER object collected
// Use for: resource cleanup, off-heap memory management

// ── MEMORY AREAS ──
// Eden Space: new objects born here (~80% of Young Gen)
// Survivor S0/S1: objects survive Minor GC, bounced between
// Old Gen: objects surviving 15 GC cycles (default, tunable)
// Metaspace: class metadata (unlimited by default in Java 8+)
// Code Cache: JIT-compiled native code
// Direct Memory: ByteBuffer.allocateDirect() (off-heap, no GC)

// ── STRING DEDUPLICATION ──
// -XX:+UseStringDeduplication (with G1 GC)
// JVM finds Strings with same content and shares their char[] array
// Saves memory when many equal strings (common in JSON/XML processing)