Skip to main content
Java advanced Lesson 40 of 58

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)

RegionContentsCollected by
EdenNewly created objectsMinor GC (fast, frequent)
Survivor 0/1Objects that survived one Minor GCMinor GC
Old GenerationLong-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 TypeHeap SizeGC
Microservice (< 1k req/s)512m–1gG1GC or ZGC
REST API (high throughput)2g–8gG1GC (-XX:MaxGCPauseMillis=100)
Low-latency trading4g–16gZGC or Shenandoah
Batch processing4g–32gParallel GC
Large data processing16g+ZGC

Frequently Asked Questions

What is the difference between heap and stack memory?
Stack memory holds method call frames — local variables, parameters, and return addresses. It is per-thread, automatically managed (grows on call, shrinks on return), and fast. Heap memory holds all objects created with 'new'. It is shared across threads, managed by the garbage collector, and is where most tuning happens.
What is Metaspace and how is it different from PermGen?
Metaspace (Java 8+) stores class metadata — class structures, method bytecode, constant pools. It replaced PermGen, which had a fixed maximum size causing java.lang.OutOfMemoryError: PermGen space. Metaspace grows dynamically in native memory (off-heap) and only causes OOM if the OS runs out of memory. You can still cap it with -XX:MaxMetaspaceSize.
What is a memory leak in Java?
A memory leak in Java occurs when objects are no longer needed by the application but are still reachable by some reference — so the GC cannot collect them. Common causes: static collections that grow unboundedly, ThreadLocal variables not removed, event listeners not deregistered, and caches without eviction.
Which GC should I use?
G1GC (default since Java 9) is a good default for most applications — balanced throughput and latency. ZGC (Java 21 production-ready) targets sub-millisecond pauses and scales to terabyte heaps. Shenandoah is similar to ZGC. Parallel GC maximises throughput for batch jobs where pause time does not matter.