Overview
Before GC makes sense you need the floor plan: the JVMS-defined runtime data areas, what
HotSpot actually does with them, and how an object is laid out and allocated. Most
"memory problems" resolve to knowing which area filled up — each one has its own
OutOfMemoryError flavour, its own sizing flag, and its own failure story.
Key points
- Heap: all objects and arrays, shared across threads, the GC's territory. Sized by
-Xms/-Xmx; divided by generational collectors into young (eden + two survivor spaces) and old generation, or into equal regions by G1/ZGC/Shenandoah. - Per-thread stacks: one frame per method call — locals, operand stack, return
address. Primitives and references live here; the objects they point to never do.
Overflow →
StackOverflowError; size per thread via-Xss(matters at thousands of platform threads — one reason virtual threads keep theirs on the heap). - Metaspace: class metadata (runtime representation of classes, method bytecode,
constant pools). Native memory, not heap — it replaced PermGen in Java 8, growing by
default until
-XX:MaxMetaspaceSize. Leaks here are classloader leaks (redeploy churn in app servers, dynamic proxy/bytecode generation gone wild). - Code cache & native: JIT-compiled code lives in the code cache; threads, DirectByteBuffers,
and JNI/FFM allocations live in plain native memory. A container OOM-kill with a healthy
heap usually means native memory — diagnose with
-XX:NativeMemoryTracking. - Object layout: a header (mark word for identity hash/lock/GC age + class pointer, compressed to 8–12 bytes) then fields, 8-byte aligned. Compressed oops keep references 4 bytes on heaps ≤ ~32 GB — crossing that threshold loses usable memory before it gains any, so 31 GB beats 33 GB.
- Allocation is cheap by design: each thread bumps a pointer inside its TLAB (thread-local allocation buffer) — no lock, ~10 instructions; escape analysis lets the JIT skip the heap entirely (scalar replacement) for objects that never leave a method.
- Reference strength: strong →
SoftReference(cleared under memory pressure — memory-sensitive caches) →WeakReference(cleared at next GC once weakly reachable — canonical maps,WeakHashMap) →PhantomReference(post-mortem cleanup via aCleaner, thefinalize()replacement). - OOME flavours name the culprit:
Java heap space(heap),GC overhead limit exceeded(heap, thrashing),Metaspace,unable to create native thread(native),Direct buffer memory(-XX:MaxDirectMemorySize). Read the message before reaching for-Xmx.
Examples
# The sizing flags that matter, in one line
java -Xms4g -Xmx4g -Xss512k -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=1g App
# What is actually filling the heap
jmap -histo:live <pid> | head -20
# Native memory breakdown (start the JVM with -XX:NativeMemoryTracking=summary)
jcmd <pid> VM.native_memory summary
Related
- Java memory & GC — the map — the group this note opens.
- GC principles & algorithms — how the heap areas drawn here get collected.
- Garbage collectors compared — how each collector carves up this same heap.
- The Java Memory Model — the ordering contract layered on top of this layout.
- jmap — histograms and heap dumps of these areas.
- jstat — live occupancy counters per space.
- JVM implementations compared — the JVMS defines these areas; implementations fill in the rest.
Citations
[1] JVMS §2.5 — Run-Time Data Areas [2] JEP 122: Remove the Permanent Generation