rgoussu@goussu: ~/library/java/memory-and-gc
~/library/java/memory-and-gc cat jvm-memory-anatomy.md

JVM memory anatomy — heap, stacks & metaspace

# The JVM's runtime data areas — heap, stacks, metaspace, code cache — plus object layout, TLAB allocation, reference strength and the OutOfMemoryError flavours.

Conceptsaved 2026-08-09 #java#jvm#memory#internals

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 a Cleaner, the finalize() 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

Citations

[1] JVMS §2.5 — Run-Time Data Areas [2] JEP 122: Remove the Permanent Generation