skip to content
everything.gc.runtimeDataAreas

the JVM's memory map

heap, stacks, metaspace, code cache. what lives where.

chapter
chapter 14.01
reading time
8 min read

a running JVM is one OS process, carved into areas. every thread gets its own stack and pc register. everyone shares the heap (objects), metaspace (classes) and the code cache (compiled code).

-Xmx only caps the heap. the rest is native memory, and it's just as real.

the picture

you've seen "stack and heap" a hundred times. that's two boxes. a real JVM has closer to ten, and the other eight are why a container running -Xmx1g can still get killed for using too much memory.

think of the JVM as an office building. each thread is an employee with a private desk (their stack) and a sticky note saying what they're doing right now (the pc register). there's one big shared warehouse (the heap) where all the actual stuff is. there's a room full of blueprints that says what every kind of thing looks like (metaspace). there's a machine shop where the JIT keeps fast, pre-built versions of your hottest methods (the code cache). and then there's the building's own plumbing: the GC's bookkeeping, I/O buffers, symbol tables. nobody puts that in the brochure, but it's on the electricity bill.

one JVM processjava -Xmx512m …
per thread× 14 threads in this run
main
C2 compiler
G1 worker
shared by every threadcapped by -Xmx
native memory the JVM managesnot counted by -Xmx
java heap
what lives here
every object and every array. string literals too (the string pool lives here), and static fields (they sit inside the Class object).
sized by
-Xmx and -Xms. without them the max is 25% of RAM: 6 GB on this 24 GB mac.
when it runs out
OutOfMemoryError: Java heap space
this run
520 MB reserved, 386 MB committed.
hover or tap any area. purple is the JVM's own machinery: class metadata, compiled code, GC bookkeeping. the numbers come from a real run (javac compiling three files on jdk 25, -Xmx512m, measured with native memory tracking).

notice the split. the top row is per thread: start a thousand threads and you get a thousand stacks. the middle is the heap, the only thing -Xmx limits. the bottom is native memory the JVM allocates for itself, mostly sized by other flags, some of it not sized by anything at all.

what the spec says, and what HotSpot does

the JVM spec lists six "run-time data areas". it says what each one is for, never where it lives. HotSpot (the JVM inside basically every JDK you'll download) maps them like this:

the spec saysHotSpotlives in
pc registerone per thread, the current bytecodethe thread
JVM stackthe thread's stack, java framesnative memory, per thread
native method stackthe same stack. HotSpot uses one stack for java and native framesnative memory, per thread
heapthe java heap, split into generations or regions by the collectorreserved at startup, capped by -Xmx
method areametaspace (plus the compressed class space)native memory
run-time constant poolpart of each class's metadatametaspace

and two that the spec doesn't mention at all: the code cache (machine code from the JIT) and everything the garbage collector needs to do its job.

one level deeper:permgen, and why it had to go+

before java 8, class metadata lived in the permanent generation: a fixed-size region next to the heap, capped by -XX:MaxPermSize. apps that loaded lots of classes (app servers redeploying, anything generating proxies) ran out and died with OutOfMemoryError: PermGen space, and the only fix was guessing a bigger number.

java 8 (JEP 122) deleted permgen. class metadata moved to metaspace, allocated in native memory per class loader, and the default max became "unlimited". interned strings and static fields had already moved onto the regular heap in java 7. so today: metadata in native memory, everything else that's an object on the heap.

see it from the inside

the JVM reports its memory pools through java.lang.management. this lists them:

Main.java
import java.lang.management.*;
 
void main() {
    for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
        long max = pool.getUsage().getMax();
        IO.println("%-9s %-34s max %s".formatted(
            pool.getType() == MemoryType.HEAP ? "heap" : "non-heap",
            pool.getName(),
            max < 0 ? "unlimited" : (max >> 20) + " MB"));
    }
}

this one needs a real JVM (JVM internals), so it doesn't run in the browser. copy it into java Main.java to try it.

on my machine (jdk 25, G1, 24 GB of RAM), sorted:

text
heap      G1 Eden Space                      max unlimited
heap      G1 Old Gen                         max 6144 MB
heap      G1 Survivor Space                  max unlimited
non-heap  CodeHeap 'non-nmethods'            max 5 MB
non-heap  CodeHeap 'profiled nmethods'       max 117 MB
non-heap  CodeHeap 'non-profiled nmethods'   max 117 MB
non-heap  Compressed Class Space             max 1024 MB
non-heap  Metaspace                          max unlimited

see the shape? the heap pools are named after the collector. run it with -XX:+UseSerialGC and you get Eden Space, Survivor Space and Tenured Gen instead. with -XX:+UseZGC: ZGC Young Generation and ZGC Old Generation. the heap's layout belongs to the GC. the non-heap pools don't change.

the 6144 MB is no accident either: with no -Xmx, the max heap is 25% of physical memory (-XX:MaxRAMPercentage=25). 24 GB / 4 = 6 GB.

quick check
which of these does NOT count against -Xmx?

reserved is not committed

here's the part that confuses everyone reading top. the JVM asks the OS for memory in two steps.

  1. reserve: "please keep this range of addresses free for me." cheap. no memory is used, it's just a promise about addresses.
  2. commit: "ok, now actually back these pages with memory." this is what costs RAM (and the OS only really hands over a page the first time something writes to it).

the JVM reserves big contiguous ranges at startup (the whole max heap, a gigabyte for class space, 240 MB for compiled code) and commits pieces as it grows.

java heap386 of 520 MB
class space1.5 of 1024 MB
code cache9.2 of 245 MB
metaspace9.0 of 64.0 MB
GC structures62.3 of 64.7 MB
thread stacks0.30 of 30.0 MB
CDS archive13.6 of 16.0 MB
reserved: address space, a promise committed: backed by real memorylog scale

whole process: 1,977.8 MB reserved, 495.4 MB committed. hover a row.

the same javac run. the outline is what was reserved, the filled bar is what was committed. hover a row. the whole process reserved almost 2 GB of addresses and used about 495 MB.

two things jump out. the class space reserves a full gigabyte and uses a megabyte and a half (it has to be one contiguous range, so it grabs it all up front). and the GC's own structures are 62 MB, committed, for a 512 MB heap. that's memory you pay for that no heap graph will ever show you.

  • ├── capped by -Xmx
  • │ ├── young: eden + survivors
  • │ └── old
  • ├── MaxMetaspaceSize
  • │ └── compressed class space1 GB reserved
  • ├── code cacheReservedCodeCacheSize
  • ├── thread stacks-Xss × threads
  • ├── GC structuresgrows with the heap
  • ├── direct buffersMaxDirectMemorySize
  • └── everything elsesymbols, JIT arenas
what -Xmx covers, and what it doesn't. only the first branch.
one level deeper:why is the class space a separate thing at all?+

every object's header holds a pointer to its class (the Klass). on a 64-bit JVM that would be 8 bytes per object. with compressed class pointers (the default), it's a 32-bit offset from a base address instead, which only works if every Klass sits inside one contiguous range. that range is the compressed class space. reserving 1 GB of addresses costs nothing, so the JVM grabs it all at startup and commits pages as classes get loaded. more on headers in object layout.

keylines
  • each thread gets a stack and a pc register. everyone shares the heap, metaspace and the code cache.
  • the heap layout belongs to the collector: eden, survivors and old for most of them.
  • class metadata lives in metaspace, in native memory, since java 8 killed permgen.
  • the JVM reserves address space up front and commits it as it grows. only committed memory costs RAM.
  • -Xmx caps the heap and nothing else. the process is always bigger.