gap·map

JVM memory & GC

[ middle depth ]

Where JVM memory actually lives

Three areas cover almost every question at this band. The heap holds every object and array you create with new; it is shared across threads and it is what the garbage collector manages. The stack is per thread: a frame per method call, holding local primitives and the references that point into the heap. The frame pops when the method returns. Class metadata (the shape of a class, its methods, the bytecode) lives in metaspace, which is native off-heap memory and replaced the old PermGen in Java 8.

void handle() {
  int n = 3;              // primitive n: on the stack frame
  byte[] buf = new byte[n]; // array object: on the heap; buf (the ref): on the stack
} // frame pops; buf is gone; the array is collectible if nothing else points to it

Deep or infinite recursion overflows the stack and throws StackOverflowError, a different failure from running out of heap. Do not conflate them.

How generational GC keeps pauses cheap

Modern collectors bet on one observation: most objects die young. So the heap's young generation is split into Eden plus two survivor spaces. New objects land in Eden. When Eden fills, a minor GC copies the handful of survivors into a survivor space and reclaims everything else by reusing the space wholesale. It never walks the dead objects one by one, which is why churning through short-lived garbage is nearly free.

Objects that keep surviving minor collections get promoted to the old generation. Cleaning the old generation is a major or full GC, and it is the expensive one. The practical takeaway: allocation rate of short-lived objects is cheap; long-lived state that keeps growing is what hurts.

Reading an OutOfMemoryError

The message after the colon tells you which memory ran out, and they need different fixes:

  • Java heap space: you are holding too many live objects. Usually a leak, sometimes a genuinely undersized heap.
  • GC overhead limit exceeded: the JVM spends over 98% of its time collecting and frees under 2%. A heap OOM wearing a different hat.
  • Metaspace: too many loaded classes. Classic in apps that redeploy repeatedly or generate proxies and lambdas at runtime.
  • unable to create new native thread: native memory or an OS thread limit, not the heap.

Wrong "I got an OutOfMemoryError, so I need a bigger -Xmx." That flag sizes the heap only. A native-thread OOM ignores it completely, and a heap leak just reaches the same wall a few hours later. Read the message first, then decide whether the fix is more memory, fewer classes, or a bug in your code that keeps objects alive.

[ senior depth ]

G1 and ZGC without the marketing

G1 has been the default since Java 9. It carves the heap into fixed-size regions (roughly 1 to 32MB, larger for big heaps) and labels each region Eden, Survivor, Old, or Humongous at runtime rather than fixing generation boundaries in address space. Each cycle it evacuates the regions with the most dead space first (garbage first) and tries to hit the pause target you set with -XX:MaxGCPauseMillis (200ms by default). It is still stop-the-world: the evacuation copy happens with app threads parked at a safepoint. It just does less per pause.

ZGC trades throughput for latency. Marking and relocation run concurrently using colored pointers and load barriers, so pause times stay under a millisecond and do not grow with heap size. That last property is the reason to reach for it: a 2GB heap and a 2TB heap pause about the same. The costs are lower throughput than G1 (often 5 to 15%) and a bigger footprint. Generational ZGC (JEP 439, Java 21; the default ZGC mode from Java 23) brought back the young/old split and made it several times more CPU-efficient than the original single-generation design.

Wrong "ZGC has no stop-the-world pauses." It has brief ones at the start and end of marking. It is low-pause, not pause-free, and it is not automatically faster: on throughput-bound batch work G1 or the Parallel collector usually wins.

Why your microbenchmark lied

Java code does not run at full speed until it is warm. Every method starts interpreted, gets promoted to C1 (quick, lightly optimized native code) once it is called enough, and only truly hot methods reach C2, which inlines aggressively, unrolls loops, and runs escape analysis to stack-allocate objects that never leave a method. This is tiered compilation.

long t = System.nanoTime();
for (int i = 0; i < 1000; i++) hotPath(i); // still interpreted or C1 here
long ns = System.nanoTime() - t;           // measures warm-up, not steady state

A few hundred iterations measure the interpreter and C1, not the C2 steady state production reaches. Worse, C2 can constant-fold or dead-code-eliminate the work you meant to time if its result is unused. That is why JMH forces warm-up iterations, blackholes results, and forks the JVM. Speculative optimizations can also deoptimize back to the interpreter when an assumption breaks, so real p99 latency has warm-up and deopt spikes a benchmark loop hides.

Leaks in a garbage-collected language

GC frees only what is unreachable, so a Java leak is a reachability bug: something you forgot about still holds a reference. The usual suspects are a static collection that grows without bound, listeners registered and never removed, ThreadLocal values left on pooled threads that never die, and caches with no eviction. Redeploy leaks show up in metaspace when a lingering reference pins an old webapp's ClassLoader so its classes cannot be unloaded.

To diagnose, take a heap dump and read the dominator tree: sort by retained size, find the object holding the retained set, then trace the path to the GC root. Interviewers grade whether you reach for that path instead of guessing, and whether you know that a bigger -Xmx postpones a leak rather than curing it.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

unlocks

more Java

was this useful?