Java Memory Architecture & Garbage Collection — A Complete Visual Guide
Every Java object is born in Eden, might survive to Old Gen, and is eventually collected. But how does the JVM decide what lives and what dies? We'll walk through every memory region, every GC generation, and every major algorithm — with live simulations you can interact with.
The JVM Memory Landscape
When your Java program runs, the JVM carves up memory into distinct regions — each with a specific purpose, lifetime policy, and GC strategy. Getting these wrong causes OutOfMemoryError, excessive GC pauses, or silent memory leaks.
At the highest level there are two worlds: the Stack (thread-local, fast, automatic) and the Heap (shared, GC-managed, where almost everything lives). But within the heap, the story gets much richer.
Interactive JVM Memory Map
Click any region to see what lives there, its size flags, and what error it throws when full.
Click a memory region above to explore it.
Memory Regions — Deep Dive
Each thread gets its own private stack. It stores stack frames — one per method call. A frame holds: local variables, operand stack, and a reference back to the constant pool.
When a method returns, its frame is instantly popped — zero GC involvement. Stack memory is blazing fast but limited. Default thread stack size is typically 512KB–1MB depending on platform.
-Xss2m or fix the recursion.
public void processOrder(Order order) {
// ↓ frame pushed — 'total' lives on THIS thread's stack
double total = order.getAmount();
double tax = calculateTax(total); // ← new frame pushed
// calculateTax() frame popped on return ↑
order.setTotal(total + tax);
// ↑ processOrder frame popped when this method returns
// All locals (total, tax) instantly gone — no GC needed
}The heap is where new MyObject() allocates memory. It's shared across all threads and managed entirely by the garbage collector. The heap is divided into generations based on the generational hypothesis: most objects die young.
-Xmx4g, fix memory leaks, or review object retention.
Metaspace stores class metadata: class structures, method bytecode, constant pools, field/method descriptors. Unlike the old PermGen, Metaspace uses native memory — it can grow beyond JVM heap limits.
By default, Metaspace is unbounded (grows until OS runs out). Always set -XX:MaxMetaspaceSize=256m in production to prevent native memory exhaustion.
# Production baseline
-XX:MetaspaceSize=128m # initial commit size
-XX:MaxMetaspaceSize=256m # hard cap — always set this!
-XX:MinMetaspaceFreeRatio=20 # GC trigger threshold
-XX:MaxMetaspaceFreeRatio=80 # shrink threshold after GCThe JIT compiler (C1/C2) translates hot bytecode into native machine code and stores it in the Code Cache. When the cache fills, JIT compilation stops and performance degrades — the JVM falls back to interpreted mode.
-XX:ReservedCodeCacheSize=256m. Visible in logs as CodeCache is full. Compiler has been disabled.
Live Object Lifecycle Simulator
Create objects and run GC cycles. Watch objects move from Eden → Survivor → Old Gen → get collected. Age counters show how many Minor GCs an object has survived.
GC Roots & Reachability
The GC starts from GC roots — a fixed set of known live references — and traces the entire object graph. Any object reachable from a root is alive. Anything unreachable is garbage.
(stack local)
@0x1001
@0x2002
@0x3003
@0x4004 ← GC
// ⚠ Common leak: static cache holding strong references forever
public class ReportCache {
// Static field → GC root → Report objects NEVER collected
private static final Map<String, Report> cache = new HashMap<>();
public static void cache(String key, Report r) {
cache.put(key, r); // grows forever — classic heap leak
}
}
// ✓ Fix: use WeakReference — GC can collect when no other strong refs exist
private static final Map<String, WeakReference<Report>> cache = new WeakHashMap<>();
// ✓ Or: use Guava/Caffeine with max-size and TTL eviction
Cache<String, Report> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();GC Algorithms — How They Work
Click an algorithm to see how it works, its pause characteristics, and when to use it.
Production JVM Flags Cheat Sheet
| Flag | Purpose | Example |
|---|---|---|
| -Xms / -Xmx | Initial / max heap size | -Xms512m -Xmx4g |
| -Xss | Thread stack size | -Xss512k |
| -XX:MaxMetaspaceSize | Cap Metaspace (always set!) | -XX:MaxMetaspaceSize=256m |
| -XX:+UseG1GC | Enable G1 garbage collector | -XX:+UseG1GC |
| -XX:MaxGCPauseMillis | G1: target max pause time (soft goal) | -XX:MaxGCPauseMillis=200 |
| -XX:+UseZGC | Enable ZGC (Java 15+ production-ready) | -XX:+UseZGC |
| -XX:+PrintGCDetails | Verbose GC logging (legacy) | -XX:+PrintGCDetails |
| -Xlog:gc* | Unified GC logging (Java 9+) | -Xlog:gc*:file=gc.log:time |
| -XX:+HeapDumpOnOutOfMemoryError | Auto heap dump on OOM | -XX:HeapDumpPath=/dumps/ |
| -XX:NewRatio | Ratio of Old to Young gen size | -XX:NewRatio=3 |
| -XX:SurvivorRatio | Ratio of Eden to one Survivor space | -XX:SurvivorRatio=8 |