JVM Memory Model
Understand JVM memory — heap, stack, metaspace, GC algorithms, G1/ZGC tuning, and diagnosing memory leaks and OutOfMemoryError.
JVM Memory Structure
┌─────────────────────────────────────────────────────────┐
│ JVM Process │
│ │
│ ┌─────────────────────────┐ ┌───────────┐ │
│ │ Heap │ │ Metaspace │ │
│ │ ┌───────┐ ┌────────┐ │ │ (native) │ │
│ │ │Young │ │ Old │ │ │ class meta │ │
│ │ │ Gen │ │ Gen │ │ │ bytecode │ │
│ │ │Eden+S0│ │ │ │ └───────────┘ │
│ │ │ +S1 │ │ │ │ │
│ │ └───────┘ └────────┘ │ ┌───────────┐ │
│ └─────────────────────────┘ │ Code Cache│ │
│ │ (JIT asm) │ │
│ Thread 1 Stack └───────────┘ │
│ ┌──────────────┐ │
│ │ Frame: main()│ Thread 2 Stack │
│ │ local vars │ ┌──────────────┐ │
│ │ Frame: foo() │ │ Frame: run() │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
Heap Generations (G1GC / Parallel GC)
| Region | Contents | Collected by |
|---|---|---|
| Eden | Newly created objects | Minor GC (fast, frequent) |
| Survivor 0/1 | Objects that survived one Minor GC | Minor GC |
| Old Generation | Long-lived objects (survived many GCs) | Major/Full GC |
Stack Memory
Each thread has its own stack. Each method call pushes a stack frame:
public class StackDemo {
public static void main(String[] args) {
int result = add(3, 4); // frame: main — local: args, result
System.out.println(result);
}
static int add(int a, int b) { // frame: add — local: a, b
int sum = a + b; // frame: add — local: a, b, sum
return sum;
} // frame: add is popped
}
Stack overflow:
static int recurse(int n) {
return recurse(n + 1); // no base case → StackOverflowError
}
Increase stack size: -Xss4m (default is 512KB – 1MB per thread).
Heap Memory and Object Lifecycle
public class HeapDemo {
public static void main(String[] args) {
// 1. Allocated in Eden
var sb = new StringBuilder();
// 2. Still reachable → promoted to Survivor on Minor GC
for (int i = 0; i < 1_000_000; i++) {
// These temporary strings are eligible for GC immediately
String temp = "item " + i;
sb.append(temp);
}
// 3. sb still reachable — promoted to Old Gen over time
System.out.println(sb.length());
// 4. sb goes out of scope here → eligible for GC
}
}
Garbage Collection Algorithms
Serial GC
-XX:+UseSerialGC
Single-threaded, stop-the-world. Only for tiny heaps or embedded environments.
Parallel GC (Throughput Collector)
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
Multi-threaded minor and major GC. Maximises throughput, accepts longer pauses. Best for batch processing.
G1GC (Default since Java 9)
-XX:+UseG1GC # default — can omit
-XX:MaxGCPauseMillis=200 # target pause goal (best effort)
-XX:G1HeapRegionSize=16m # region size (1MB–32MB)
-XX:InitiatingHeapOccupancyPercent=45
Divides heap into equal-sized regions. Collects the regions with the most garbage first (Garbage First). Balances throughput and pause times well.
ZGC (Java 21 — production-ready)
-XX:+UseZGC
-XX:SoftMaxHeapSize=12g # ZGC can use more but tries to stay under
-XX:ZCollectionInterval=5 # seconds between GC cycles
Sub-millisecond pauses regardless of heap size (tested up to terabytes). All GC work is concurrent with the application. Ideal for latency-sensitive services.
Shenandoah
-XX:+UseShenandoahGC
Similar goals to ZGC, developed by Red Hat. Available in OpenJDK.
JVM Flags — Essential Tuning
# Heap size
-Xms512m # initial heap size
-Xmx2g # maximum heap size
-Xms2g -Xmx2g # fix initial=max to avoid resizing
# Stack size per thread
-Xss512k
# Metaspace
-XX:MetaspaceSize=256m # initial metaspace size
-XX:MaxMetaspaceSize=512m # cap metaspace
# GC logging (Java 9+)
-Xlog:gc*:file=gc.log:time,uptime,level,tags
# GC tuning
-XX:MaxGCPauseMillis=100 # G1: target max pause (best effort)
-XX:+AlwaysPreTouch # touch all heap pages at JVM start (reduces latency spikes)
# Diagnostics
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
Diagnosing Memory Issues
OutOfMemoryError: Java heap space
# 1. Enable heap dump on OOM
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof MyApp
# 2. Analyse the dump with Eclipse Memory Analyser (MAT) or VisualVM
# Look for the "leak suspects" report — usually shows the largest retained objects
# 3. Increase heap as a temporary measure while you fix the leak
java -Xmx4g MyApp
Common Memory Leak Patterns
// 1. Static collection that grows forever
public class LeakyCache {
// Bad — never evicted
private static final Map<String, byte[]> cache = new HashMap<>();
public static void store(String key, byte[] data) {
cache.put(key, data); // grows until OOM
}
// Fix — use a bounded cache (Caffeine, Guava Cache, or LinkedHashMap with removeEldest)
private static final Map<String, byte[]> bounded =
Collections.synchronizedMap(new java.util.LinkedHashMap<>() {
@Override protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
return size() > 1000;
}
});
}
// 2. Listeners not removed
public class EventSource {
private final List<Listener> listeners = new ArrayList<>();
public void addListener(Listener l) { listeners.add(l); }
public void removeListener(Listener l) { listeners.remove(l); } // must call this!
}
// 3. ThreadLocal not cleaned up
ThreadLocal<byte[]> buffer = new ThreadLocal<>();
try {
buffer.set(new byte[1024 * 1024]);
doWork();
} finally {
buffer.remove(); // MUST call remove() or the value leaks with the thread
}
Java Flight Recorder (JFR)
# Record 60 seconds of JFR data
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr MyApp
# Or attach to a running process
jcmd <pid> JFR.start duration=60s filename=recording.jfr
jcmd <pid> JFR.dump filename=recording.jfr
// Programmatic JFR events
import jdk.jfr.*;
@Label("Order Processed")
@Category("Business")
@StackTrace(false)
public class OrderEvent extends Event {
@Label("Order ID")
public String orderId;
@Label("Amount")
public double amount;
}
// Usage
var event = new OrderEvent();
event.orderId = "ORD-42";
event.amount = 99.99;
event.begin();
processOrder(event.orderId);
event.commit(); // committed only if enabled in JFR config
Object References and GC
Java has four reference strengths:
import java.lang.ref.*;
// Strong (default) — GC never collects while reachable
String s = "hello";
// Soft — GC collects when memory is low (good for caches)
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
byte[] data = softRef.get(); // may return null if GC'd
// Weak — GC collects at next GC cycle (used in WeakHashMap)
WeakReference<MyObject> weakRef = new WeakReference<>(new MyObject());
MyObject obj = weakRef.get(); // returns null after GC
// Phantom — for post-mortem cleanup (Java 9 Cleaner is preferred)
ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
PhantomReference<MyObject> phantom = new PhantomReference<>(new MyObject(), queue);
// WeakHashMap — keys held weakly; entry removed when key is GC'd
Map<Object, String> weakMap = new WeakHashMap<>();
Memory Sizing Guidelines
| App Type | Heap Size | GC |
|---|---|---|
| Microservice (< 1k req/s) | 512m–1g | G1GC or ZGC |
| REST API (high throughput) | 2g–8g | G1GC (-XX:MaxGCPauseMillis=100) |
| Low-latency trading | 4g–16g | ZGC or Shenandoah |
| Batch processing | 4g–32g | Parallel GC |
| Large data processing | 16g+ | ZGC |