Java Memory Deep Dive
Part 1: JVM Memory Areas
The Big Picture: JVM Memory Architecture
When you run java MyApp, the JVM carves out memory from the OS into distinct regions, each with a specific purpose, lifecycle, and failure mode.
[!IMPORTANT] Interview Key: The JVM spec defines logical areas. The actual implementation (HotSpot, OpenJ9, GraalVM) decides how they're laid out in physical memory.
The Heap — Where Objects Live
The heap is the runtime data area from which memory for all class instances and arrays is allocated. It is created on JVM startup and shared among all threads.
Heap Structure (Generational Layout)
Why Generational?
The Weak Generational Hypothesis states:
Most objects die young — Temporary variables, iterator objects, lambda captures
Few objects survive long — Caches, singletons, connection pools
This observation drives the entire GC strategy:
| Property | Young Generation | Old Generation |
|---|---|---|
| Object Lifespan | Short-lived (>90% die here) | Long-lived |
| GC Frequency | Very frequent (Minor GC) | Infrequent (Major/Full GC) |
| GC Algorithm | Copying collector (fast) | Mark-Sweep-Compact (slower) |
| Pause Impact | Short pauses (ms) | Longer pauses (ms to seconds) |
| Size | Typically 1/3 of heap | Typically 2/3 of heap |
Eden Space — The Birthplace
// Every time you do this, Eden allocates memory:
Object obj = new Object(); // ~16 bytes in Eden
String name = new String("hello"); // ~56 bytes in Eden
int[] arr = new int[100]; // ~416 bytes in Eden
How Eden allocation works (TLAB — Thread-Local Allocation Buffer):
[!TIP] TLAB (Thread-Local Allocation Buffer) eliminates contention. Each thread gets its own chunk of Eden. Allocation = simple pointer bump (no locking). When a TLAB fills up, the thread requests a new one.
Survivor Spaces — The Proving Ground
After a Minor GC, surviving objects are copied between S0 and S1 in a ping-pong fashion:
Key parameters:
-XX:MaxTenuringThreshold=15(default) — max age before promotion-XX:SurvivorRatio=8— Eden:Survivor ratio (8:1:1)
Old Generation — The Retirement Home
Objects arrive here through:
Age-based promotion — Survived enough Minor GC cycles
Premature promotion — Survivor space overflow
Large object allocation — Objects too big for Young Gen (
-XX:PretenureSizeThreshold)Dynamic age computation — If objects of a certain age fill >50% of Survivor, promote earlier
Heap Sizing Flags Summary
| Flag | Purpose | Example |
|---|---|---|
-Xms |
Initial heap size | -Xms512m |
-Xmx |
Maximum heap size | -Xmx4g |
-Xmn |
Young generation size | -Xmn256m |
-XX:NewRatio |
Old:Young ratio | -XX:NewRatio=2 (Old is 2x Young) |
-XX:SurvivorRatio |
Eden:Survivor ratio | -XX:SurvivorRatio=8 |
-XX:MaxTenuringThreshold |
Promotion age | -XX:MaxTenuringThreshold=15 |
The Stack — Where Method Execution Lives
Each thread gets its own stack (-Xss, default ~512KB–1MB). The stack stores frames — one per method invocation.
Stack Frame Anatomy
What Each Frame Contains
Stack vs Heap — The Critical Distinction
public void example() {
int x = 42; // x on STACK (primitive)
String name = "hello"; // name ref on STACK, "hello" on HEAP (string pool)
Object obj = new Object(); // obj ref on STACK, Object instance on HEAP
int[] arr = new int[5]; // arr ref on STACK, array on HEAP
}
[!IMPORTANT] Interview Answer: "Primitives and references live on the stack. Objects live on the heap. The stack reference points to the heap object. When the stack frame pops, the reference is gone, but the heap object remains until GC collects it."
StackOverflowError
// Classic cause: unbounded recursion
public int factorial(int n) {
return n * factorial(n - 1); // Missing base case!
}
// Each call adds a frame. Stack exhausted = StackOverflowError
Tuning: -Xss2m increases stack size per thread, but more threads = more memory consumed.
Metaspace — Where Class Metadata Lives
Replaced PermGen in Java 8. Lives in native memory (not the heap).
What Metaspace Stores
PermGen vs Metaspace
| Feature | PermGen Java 7 and earlier | Metaspace Java 8 and later |
|---|---|---|
| Location | JVM Heap (contiguous) | Native memory |
| Default Size | 64MB–256MB fixed | Unlimited (auto-grows) |
| OOM Error | OutOfMemoryError: PermGen space |
OutOfMemoryError: Metaspace |
| GC | Full GC only | Full GC + class unloading |
| Tuning | -XX:MaxPermSize |
-XX:MaxMetaspaceSize |
[!WARNING] Common Leak: Frameworks that create dynamic classes (CGLIB proxies, Groovy scripts, heavy reflection) can leak Metaspace if classloaders are not properly garbage collected.
Code Cache — JIT Compiled Code
The Just-In-Time (JIT) compiler translates hot bytecode into native machine code. That compiled code lives in the Code Cache.
Segmented Code Cache (Java 9+): Split into three segments for better sweeping and management.
| Flag | Purpose | Default |
|---|---|---|
-XX:ReservedCodeCacheSize |
Total code cache | 240MB |
-XX:InitialCodeCacheSize |
Initial size | 2.5MB |
Direct (Off-Heap) Memory
Used by java.nio for I/O buffers. Not managed by GC — must be explicitly freed or rely on Cleaner/finalizers.
// Allocated in native memory, NOT on the heap
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024 * 1024); // 1MB
// vs. heap buffer (backed by byte[] on the heap)
ByteBuffer heapBuf = ByteBuffer.allocate(1024 * 1024);
| Flag | Purpose |
|---|---|
-XX:MaxDirectMemorySize |
Cap on direct memory (default approx equal to -Xmx) |
Complete Memory Map
[!CAUTION] Why your container gets OOM-killed:
docker run -m 512m java -Xmx512mwill fail! The JVM process needs heap + metaspace + threads + code cache + direct memory + GC overhead. Rule of thumb: set-Xmxto ~70-75% of container memory.
FAQs — Memory Areas
Q1: What is the difference between stack and heap memory?
Stack is per-thread, stores frames (local variables, operand stack, return addresses). It is LIFO, fixed-size (-Xss), and automatically managed. Heap is shared, stores all object instances, dynamically sized (-Xms/-Xmx), and managed by GC. Primitives and references go on stack; objects go on heap.
Q2: Why did Java replace PermGen with Metaspace?
PermGen had a fixed max size on the heap, causing frequent OutOfMemoryError: PermGen space especially with dynamic class generation. Metaspace uses native memory, auto-grows by default, and allows better class unloading. It decouples class metadata from the heap, simplifying GC.
Q3: Can objects live on the stack?
Yes! Through Escape Analysis (Java 6+). If the JIT compiler determines an object does not escape the method scope, it can perform scalar replacement — decomposing the object into its primitive fields on the stack, avoiding heap allocation entirely. Enabled by default with -XX:+DoEscapeAnalysis.
Q4: What happens when Eden is full?
A Minor GC (Young GC) is triggered. The GC identifies live objects in Eden and the active Survivor space using reachability analysis from GC roots. Live objects are copied to the other Survivor space (or promoted to Old Gen if old enough). Eden and the previous Survivor are cleared.
Q5: A container with 1GB memory runs a Java app with -Xmx1g. It keeps getting OOM-killed. Why?
-Xmx only controls heap. Total JVM memory = heap + metaspace + code cache + thread stacks + direct buffers + GC overhead + native libraries. Set -Xmx to ~70% of container memory, or use -XX:MaxRAMPercentage=75.0 to let JVM auto-calculate.