JVM Memory Model
The JVM Memory Model encompasses two related but distinct concepts. The first is the runtime memory architecture — how the JVM partitions memory into regions (stack, heap, Metaspace, code cache, native memory) and what each region stores. The second, more precise meaning is the Java Memory Model (JMM) defined in the Java Language Specification — the formal set of rules that govern how multithreaded programs observe memory: when writes made by one thread become visible to other threads, what ordering guarantees exist, and how volatile, synchronized, and final provide those guarantees. This entry covers both: the complete runtime memory architecture with all regions explained, and the Java Memory Model's happens-before relationship, visibility rules, and the practical consequences for concurrent code.
JVM Runtime Memory Architecture — All Regions
// ── Complete JVM memory picture ──────────────────────────────────────
//
// ┌────────────────────────────────────────────────────────────────────┐
// │ JVM PROCESS MEMORY │
// │ │
// │ ┌─────────────────────────────────────────────────────────────┐ │
// │ │ JAVA HEAP │ │
// │ │ Eden | Survivor | Old Gen | Humongous (G1GC regions) │ │
// │ │ Controlled by -Xms and -Xmx │ │
// │ └─────────────────────────────────────────────────────────────┘ │
// │ │
// │ ┌──────────────────────────┐ ┌──────────────────────────────┐ │
// │ │ METASPACE │ │ CODE CACHE │ │
// │ │ Class metadata │ │ JIT-compiled native code │ │
// │ │ Native memory │ │ -XX:ReservedCodeCacheSize │ │
// │ │ -XX:MaxMetaspaceSize │ └──────────────────────────────┘ │
// │ └──────────────────────────┘ │
// │ │
// │ ┌──────────────────────────┐ ┌──────────────────────────────┐ │
// │ │ THREAD STACKS (×N) │ │ DIRECT MEMORY │ │
// │ │ Per-thread stack │ │ ByteBuffer.allocateDirect() │ │
// │ │ -Xss per thread │ │ -XX:MaxDirectMemorySize │ │
// │ └──────────────────────────┘ └──────────────────────────────┘ │
// │ │
// │ ┌──────────────────────────────────────────────────────────────┐ │
// │ │ NATIVE MEMORY (JVM internals) │ │
// │ │ GC metadata, JVM structures, JNI, symbols, etc. │ │
// │ └──────────────────────────────────────────────────────────────┘ │
// └────────────────────────────────────────────────────────────────────┘
// ── Total process memory = sum of all regions ─────────────────────────
// Common mistake: setting -Xmx to available RAM leaves no room for other regions
// Typical non-heap memory (for a medium Spring Boot app):
// Metaspace: 150-300MB
// Code Cache: 50-150MB
// Thread stacks: threads × Xss (100 threads × 512KB = 50MB)
// Direct memory: depends on NIO usage
// GC overhead: ~15-30% of heap size
//
// Rule of thumb: containerMemory - Xmx should be >= 500MB for non-heap
// ── Monitoring all memory regions ─────────────────────────────────────
// jcmd <pid> VM.native_memory (full breakdown including all regions)
// Output:
// Native Memory Tracking:
// Total: reserved=4183MB, committed=1234MB
// Java Heap (reserved=2048MB, committed=512MB)
// Class (reserved=1085MB, committed=42MB) ← Metaspace
// Thread (reserved=166MB, committed=166MB) ← Stack for all threads
// Code (reserved=240MB, committed=12MB) ← Code Cache
// GC (reserved=195MB, committed=195MB) ← GC data structures
// Symbol (reserved=17MB, committed=17MB) ← String pool symbolsThe Java Memory Model — Visibility and Happens-Before
// ── Visibility problem without synchronisation ───────────────────────
public class VisibilityProblem {
private boolean stopRequested = false; // non-volatile shared variable
public void start() {
Thread worker = new Thread(() -> {
while (!stopRequested) { // may NEVER see stopRequested = true!
doWork();
}
});
worker.start();
Thread.sleep(1000);
stopRequested = true; // main thread writes
// No happens-before between this write and the worker's read
// Worker may loop forever — JIT may hoist the read out of the loop
}
}
// ── Fix 1: volatile establishes happens-before ───────────────────────
public class VisibilityFixed {
private volatile boolean stopRequested = false;
// ↑ volatile write happens-before subsequent volatile read
public void start() {
Thread worker = new Thread(() -> {
while (!stopRequested) { // guaranteed to see the write
doWork();
}
});
worker.start();
Thread.sleep(1000);
stopRequested = true; // volatile write — establishes happens-before
}
}
// ── Fix 2: synchronized establishes happens-before ────────────────────
public class SynchronizedFixed {
private boolean stopRequested = false; // protected by 'this' monitor
public synchronized void requestStop() {
stopRequested = true; // monitor unlock happens-before
}
public synchronized boolean isStopped() {
return stopRequested; // monitor lock happens-after
}
}
// ── The happens-before rules in code ──────────────────────────────────
// Rule 1: Program order within a thread
int x = 1; // hb
int y = 2; // hb (y write guaranteed to see x write within same thread)
// Rule 2: Monitor unlock → lock
synchronized (lock) { sharedData = 42; } // unlock hb
synchronized (lock) { System.out.println(sharedData); } // subsequent lock
// Rule 3: Volatile write → read
volatile int counter = 0;
counter = 1; // volatile write hb
int v = counter; // subsequent volatile read: guaranteed to see 1
// Rule 4: Thread.start() → all actions in started thread
sharedResource = "initialised";
Thread t = new Thread(() -> {
System.out.println(sharedResource); // guaranteed to see "initialised"
});
t.start(); // start() hb all actions in t
// Rule 5: All thread actions hb join() return
Thread worker = new Thread(() -> { result = computeResult(); });
worker.start();
worker.join();
System.out.println(result); // guaranteed to see result computed by workervolatile, synchronized, and final — Memory Semantics
// ── volatile — visibility + ordering, no atomicity ───────────────────
public class VolatileCounter {
private volatile int count = 0;
// WRONG — count++ is read-modify-write (three operations), not atomic:
public void increment() {
count++; // Not thread-safe even with volatile!
// Thread 1: read count (5), increment (6), write (6)
// Thread 2: read count (5), increment (6), write (6) ← lost update!
}
// Use AtomicInteger for atomic increment:
private final AtomicInteger atomicCount = new AtomicInteger(0);
public void safeIncrement() { atomicCount.incrementAndGet(); }
// volatile IS appropriate for status flags (single writer, many readers):
private volatile boolean running = false;
public void start() { running = true; }
public void stop() { running = false; }
public boolean isRunning() { return running; }
}
// ── synchronized — mutual exclusion + happens-before ──────────────────
public class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++; // read-modify-write safely inside synchronized
}
public synchronized int getCount() {
return count; // always returns current value
}
}
// ── final — safe publication without synchronisation ──────────────────
// UNSAFE publication (without final or synchronisation):
public class UnsafePublication {
public int x;
public int y;
public UnsafePublication(int x, int y) { this.x = x; this.y = y; }
}
// Another thread receiving reference to UnsafePublication may see x=0 or y=0
// even if constructor ran completely — without happens-before, no guarantee
// SAFE publication via final fields:
public class SafePublication {
public final int x;
public final int y;
public SafePublication(int x, int y) { this.x = x; this.y = y; }
}
// JMM final-field guarantee: any thread seeing a reference to SafePublication
// is guaranteed to see x and y correctly — no synchronisation needed for reading
// SAFE publication via volatile field holding the reference:
private volatile SafePublication pub;
// Thread 1: pub = new SafePublication(3, 4); volatile write hb
// Thread 2: SafePublication p = pub; volatile read → sees x=3, y=4
// ── Double-checked locking — correct implementation ───────────────────
public class Singleton {
private static volatile Singleton instance; // volatile required!
public static Singleton getInstance() {
if (instance == null) { // first check (no lock)
synchronized (Singleton.class) {
if (instance == null) { // second check (with lock)
instance = new Singleton(); // volatile write → visible to all
}
}
}
return instance;
}
// Without volatile: the partially constructed Singleton may be visible
// (reference published before object fully initialised)
// With volatile: construction completes hb volatile write hb first check
}Memory Model Practical Guide — Common Scenarios
// ── Scenario 1: One thread initialises, many threads read ────────────
// WRONG — no happens-before between init and reads:
static Config config; // non-volatile
Thread initThread = new Thread(() -> config = Config.load());
initThread.start();
initThread.join();
// join() establishes happens-before! After join(), config IS visible
// (join() is one of the happens-before rules)
System.out.println(config); // safe — join hb this read
// But without join():
executor.submit(() -> config = Config.load());
// Other threads accessing config without join() have no happens-before guarantee
// ── Scenario 2: Lazy initialisation (thread-safe) ─────────────────────
// Initialisation-on-demand holder idiom:
public class Registry {
private Registry() { /* expensive init */ }
private static class Holder {
// static field initialization is performed under class loading lock
// Class loading is thread-safe by JVM specification
static final Registry INSTANCE = new Registry();
}
public static Registry getInstance() {
return Holder.INSTANCE;
// First access loads Holder — thread-safe class initialisation
// No explicit synchronisation needed
}
}
// ── Scenario 3: Producer-consumer with BlockingQueue ──────────────────
// BlockingQueue provides happens-before: put() hb take()
// Anything done before put() is visible after take()
BlockingQueue<Work> queue = new LinkedBlockingQueue<>();
// Producer thread:
Work work = new Work(data);
queue.put(work); // happens-before any subsequent take()
// Consumer thread:
Work taken = queue.take();
taken.process(); // guaranteed to see work fully initialised
// ── Scenario 4: AtomicReference for lock-free update ─────────────────
AtomicReference<Config> configRef =
new AtomicReference<>(Config.load());
// Any thread can update config atomically:
Config newConfig = Config.load();
configRef.set(newConfig); // atomic + happens-before for readers
// Any thread reads the current config:
Config current = configRef.get(); // reads latest value
// Compare-and-swap for conditional update:
Config expected = configRef.get();
Config updated = expected.withTimeout(30);
boolean swapped = configRef.compareAndSet(expected, updated);
// swapped = true if expected was still current — atomic
// ── Summary: when to use what ─────────────────────────────────────────
// volatile: single writer, multiple readers, no compound operations
// synchronized: compound operations, critical sections, notification (wait/notify)
// AtomicXxx: single-variable compound operations (CAS, increment)
// java.util.concurrent: collections, higher-level coordination
// Immutability: shared objects that do not change — safest approach