Command Palette

Search for a command to run...

PHASE 14Advanced ~33 min· topic 2 of 6

Topic 14.2

JVM Memory Areas

In one line

A running JVM uses several separate memory areas: one shared heap for objects, one stack per thread for method frames, Metaspace for class metadata, a code cache for JIT-compiled code, and other native memory such as direct buffers and thread stacks. Each has its own limit, its own flag, and its own kind of OutOfMemoryError or StackOverflowError.

Think of it like this

An office building. Everyone shares the big warehouse in the basement where the goods are stored (the heap). Each worker has a personal desk with a pile of sticky notes, one note per task in progress, the newest on top (that worker's stack). The company handbook describing every job lives in the library (Metaspace). The speed-dial cards that experienced workers wrote for their most frequent tasks are kept in a card box (the code cache). And some things are rented in a storage unit across the street, outside the building's own floor plan (native and direct memory). Running out of space in any one of these is a different problem with a different fix.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Heap
The shared memory area where every object and array lives. The garbage collector cleans it.
Stack frame
The block of memory for one method call in progress: its local variables, its working values, and where to return to.
Metaspace
Native memory where HotSpot keeps class metadata (field layouts, method bytecode, constant pools). Replaced PermGen in Java 8.
Native memory
Memory the JVM process gets straight from the operating system, outside the Java heap. The garbage collector doesn't manage it.
Code cache
The memory area where the JIT compiler stores the machine code it generates.
Direct buffer
A ByteBuffer whose bytes live in native memory instead of a byte[] on the heap, so the operating system can read and write them without copying.
Object header
The hidden bytes at the start of every object: lock and hash information plus a pointer to the object's class.
Compressed oops
Storing object references as 32-bit numbers instead of 64-bit ones, which saves memory when the heap is smaller than about 32 GB. "Oop" means ordinary object pointer.
TLAB
Thread-local allocation buffer: a chunk of the heap reserved for one thread, so it can create objects without coordinating with other threads.

Step by step

01The map of a running JVM

The JVM Specification (chapter 2.5) defines the run-time data areas: the pc register, JVM stacks and native method stacks (one per thread), and the heap, method area and run-time constant pool (shared). HotSpot adds the code cache and plenty of other native memory. The diagram shows which parts are shared and which belong to a single thread.

The map of a running JVMdiagram
Rendering diagram…

02Inside a stack frame

Compile static int add(int a, int b) { int sum = a + b; return sum; } and look at it with javap -c. The frame has local variable slots (0 = a, 1 = b, 2 = sum) and an operand stack: iload_0 pushes a, iload_1 pushes b, iadd pops both and pushes the sum, istore_2 stores it. A long or double takes two slots; an instance method's slot 0 is this.

The compiler computes each method's maximum stack depth and number of locals, so a frame's size is known before the method runs. That's why stack allocation is so fast: just move the stack pointer.

terminal
$ javap -c Calc.class
── expected output ──
static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn

03Allocation: TLABs and bump pointers

Every thread gets a TLAB, a private slice of the young generation. new Point(1, 2) checks that the TLAB has 24 bytes left, advances a pointer by 24, writes the header and zeroes the fields. No lock is taken. When the TLAB is full the thread asks for a new one, and when the young generation is full a garbage collection starts (Topic 14.3).

Very large arrays skip the TLAB. In G1, an object of at least half a region is a humongous object allocated straight into contiguous old-generation regions, which is why huge short-lived arrays are expensive under G1.

04Object layout and what an object really costs

A Point { int x; int y; } is 12 bytes of header plus 8 bytes of fields = 20, padded to 24 bytes. An int[10] is 16 bytes of header and length plus 40 bytes = 56. An ArrayList<Integer> of 1,000 numbers holds 1,000 references (4 bytes each) plus 1,000 Integer objects (16 bytes each, except the cached ones) plus the list itself, roughly 20 KB for 4 KB of data.

The OpenJDK tool JOL (Java Object Layout) prints exact layouts. The sample output below is from a typical 64-bit JVM with compressed oops and class pointers.

terminal
$ java -jar jol-cli.jar internals java.lang.Integer # sample output, abbreviated
── expected output ──
java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00045ad8
12 4 int Integer.value 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

05Which OutOfMemoryError is it?

The message after java.lang.OutOfMemoryError: tells you which area ran out, and each needs a different fix.

Java heap space: the heap is full of live objects (a leak, or the heap is too small). GC overhead limit exceeded (Parallel collector): the JVM spends nearly all its time collecting and frees almost nothing. Metaspace or Compressed class space: too many classes, usually a class-loader leak. Direct buffer memory: direct ByteBuffers exceed MaxDirectMemorySize. unable to create native thread: the OS refused another thread (process limits or memory). Requested array size exceeds VM limit: an array longer than the JVM allows, whatever the heap size.

06Sizing a JVM in a container

In a container with a 2 GB memory limit, the JVM's default max heap is 512 MB (25%). Many teams raise it with -XX:MaxRAMPercentage=75, leaving the rest for Metaspace, thread stacks, the code cache, direct buffers and GC structures. Setting -Xmx equal to the container limit is a classic mistake: the heap fits, the process doesn't, and the kernel's OOM killer ends it with exit code 137 and no Java error at all.

terminal
$ java -XX:MaxRAMPercentage=75 -XX:+PrintFlagsFinal -version | grep -E 'MaxHeapSize|MaxRAMPercentage' # sample output in a 2 GB container
── expected output ──
size_t MaxHeapSize = 1610612736 {product} {ergonomic}
double MaxRAMPercentage = 75.000000 {product} {command line}

07Seeing native memory with NMT

Start the JVM with -XX:NativeMemoryTracking=summary (a small overhead), then ask it for a report. The sample output is abbreviated; numbers vary.

terminal
$ jcmd 4242 VM.native_memory summary # sample output, abbreviated
── expected output ──
Native Memory Tracking:
 
Total: reserved=1745879KB, committed=325783KB
- Java Heap (reserved=262144KB, committed=67584KB)
- Class (reserved=1048894KB, committed=5310KB)
(classes #6720)
- Thread (reserved=21579KB, committed=1167KB)
(thread #21)
- Code (reserved=247977KB, committed=8105KB)
- GC (reserved=58316KB, committed=55456KB)

Try it yourself

  1. 1

    Shrink the stack

    On your own JDK, change the first example to print depth itself, then run it with java -Xss256k Main.java and with java -Xss4m Main.java. The depth changes roughly in proportion. Add a few extra local variables to dive and run again: bigger frames, fewer of them fit.

  2. 2

    Fill the heap on purpose

    Write a loop that adds new byte[1_000_000] to an ArrayList forever and run it with java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError Main.java. Predict the message (Java heap space), then open the .hprof file it writes in a heap analyser such as Eclipse MAT or VisualVM and find the ArrayList holding everything (Topic 14.5).

  3. 3

    Move the Integer cache

    Run the third example with java -XX:AutoBoxCacheMax=1000 Main.java. Predict line 2 first. (It becomes true: HotSpot lets you raise the upper bound of the Integer cache, which is one more reason never to rely on == for boxed values.)

Code & diagrams

Private stacks, shared heap, and StackOverflowError New tab

The exact depth depends on -Xss and frame sizes, so the program prints only whether it passed 1000. A StackOverflowError only affects the thread that hit it.

Sign in to run this example in your browser.

Expected output

each thread counted its own local to 5
shared box on the heap = 10
StackOverflowError caught, more than 1000 frames deep: true
stack unwound; main carries on
Heap limits vs native memory New tab

HotSpot rejects an array this long before even looking at the heap size, so the message is the same on every machine. Catching OutOfMemoryError is for the demo only.

Sign in to run this example in your browser.

Expected output

OutOfMemoryError: Requested array size exceeds VM limit
heap buffer:   direct=false, has byte[]=true
direct buffer: direct=true, has byte[]=false
total capacity: 2048 bytes
Caches that live in the heap Java 5+ New tab

Integer.valueOf returns shared objects for -128 to 127 (the cache is required by the JLS for that range), so == happens to work there and fails above it. Always compare boxed values with equals.

Sign in to run this example in your browser.

Expected output

127 == 127 (boxed): true
128 == 128 (boxed): false
128 equals 128:     true
literal == folded literal: true
literal == runtime concat: false
literal == s3.intern():    true
Memory flags you'll actually use (reference)bash
# heap
-Xms512m -Xmx2g                     # initial and maximum heap
-XX:MaxRAMPercentage=75             # max heap as % of container/host memory (JDK 10+)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps
-XX:+ExitOnOutOfMemoryError         # die fast so the orchestrator restarts you

# other areas
-Xss512k                            # per-thread stack size
-XX:MaxMetaspaceSize=256m           # cap class metadata (unlimited by default)
-XX:ReservedCodeCacheSize=256m      # JIT code cache
-XX:MaxDirectMemorySize=512m        # direct ByteBuffers (default: same as max heap)

# look inside
-XX:NativeMemoryTracking=summary    # then: jcmd <pid> VM.native_memory summary

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Metaspace capped too low

Run a program that loads a few thousand classes, such as the javac compiler itself, with a tiny Metaspace cap.

terminal
$ java -XX:MaxMetaspaceSize=6m -m jdk.compiler/com.sun.tools.javac.Main Main.java
── what you'll see ──
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.base/java.lang.ClassLoader.defineClass2(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1118)
...

Break #2

Heap as big as the container

Deploy with -Xmx2g in a container whose memory limit is 2 GiB.

terminal
$ kubectl get pod orders-7d9f -o jsonpath='{.status.containerStatuses[0].lastState}'
── what you'll see ──
{"terminated":{"exitCode":137,"reason":"OOMKilled", ...}}

Myth vs fact

Myth

Objects created inside a method live on the stack.

Fact

In Java every object is on the heap; only its reference is in the frame. The JIT may avoid the allocation completely with escape analysis (Topic 14.4), but that's an optimisation, not a rule you can rely on.

Myth

-Xmx is how much memory the JVM uses.

Fact

-Xmx limits only the heap. Metaspace, thread stacks, the code cache, direct buffers and the JVM's own structures come on top.

Myth

Static variables live in Metaspace (or PermGen).

Fact

In modern HotSpot, static field values live in the heap with the class's Class object. Metaspace holds the class's description, not its values.

Myth

A bigger heap is always faster.

Fact

A bigger heap means fewer collections, but each full scan has more to cover, and heaps over about 32 GB lose compressed oops so every reference grows to 8 bytes.

When it breaks

A service using Netty slowly grows in resident memory while the heap looks flat, until the container is killed.

What you see

Restarts every few days; heap dumps show nothing wrong because the growth is in direct buffers or other native memory.

Fix & prevent

Turn on NMT and compare summaries over time (jcmd <pid> VM.native_memory baseline, later summary.diff). Cap -XX:MaxDirectMemorySize, enable the library's leak detector, and make sure pooled buffers are released.

A batch job creating one platform thread per file hits OutOfMemoryError: unable to create native thread with plenty of heap free.

What you see

The job crashes on large inputs; raising -Xmx makes it worse because it leaves less memory for thread stacks.

Fix & prevent

Use a bounded pool (Topic 13.6) or virtual threads (Topic 13.10) instead of a thread per task; check ulimit -u and container PID limits.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Compressed oops store a 32-bit offset that the JVM shifts left by 3 (objects are 8-byte aligned), addressing 32 GB. With -XX:ObjectAlignmentInBytes=16 the limit doubles at the cost of more padding. Check with java -Xmx40g -XX:+PrintFlagsFinal -version | grep UseCompressedOops.

  • ▸

    Compact object headers (JEP 450, experimental in JDK 24, product option in JDK 25 via -XX:+UseCompactObjectHeaders) shrink the header to 8 bytes by packing the class pointer into the mark word, cutting heap use for small-object-heavy apps.

  • ▸

    The JIT may scalar-replace an object that never escapes a method: its fields become locals in registers and no heap allocation happens at all (Topic 14.4). This is why short-lived iterator and wrapper objects are often free in hot code.

  • ▸

    Thread stacks are reserved virtual memory, committed page by page as they're used, with guard pages at the end. That's how the JVM turns running off the end of the stack into a StackOverflowError instead of a crash. Very deep native or JNI frames can still crash the process.

Remember this

  1. 1

    The heap holds every object and array, shared by all threads, and is managed by the garbage collector (Topic 14.3). Its size is set with -Xms (initial) and -Xmx (maximum); without them the JVM picks defaults from the machine or container: a maximum of 1/4 of available memory (-XX:MaxRAMPercentage=25) and an initial size of 1/64. Since JDK 10 (and 8u191) the JVM reads container limits from cgroups, so in Docker or Kubernetes "available memory" means the container's limit. Static field values, the string pool (Topic 6.1) and the Integer cache are all ordinary heap objects too.

  2. 2

    Each thread has its own JVM stack of frames, one frame per method call in progress (Topic 3.11). A frame holds the method's local variables (primitives and references, never objects themselves), an operand stack that bytecode instructions push and pop, and a link back to the caller. Frames are created on call and thrown away on return, so stack memory needs no garbage collection. The size is fixed per thread with -Xss (typically 1 MB by default on 64-bit Linux); recursion that is too deep throws StackOverflowError. Each thread also has a program counter register and, for native code, a native stack.

  3. 3

    The JVM Specification calls the place for class data the method area; HotSpot implements it as Metaspace since Java 8, when it replaced the fixed-size PermGen (-XX:MaxPermSize is gone). Metaspace is native memory outside the heap and holds class structures, method bytecode, constant pools and profiling data. It grows on demand and is unlimited by default, so set -XX:MaxMetaspaceSize to catch class-loader leaks (Topic 14.1). With compressed class pointers, class structures sit in a separate compressed class space (1 GB reserved by default, -XX:CompressedClassSpaceSize).

  4. 4

    The code cache holds machine code produced by the JIT (Topic 14.4); since JDK 9 it is split into segments for JVM-internal code, profiled code and fully optimised code. When it fills up the JIT stops compiling and logs CodeCache is full. Compiler has been disabled., and performance quietly drops. The default reserved size with tiered compilation is 240 MB (-XX:ReservedCodeCacheSize).

  5. 5

    Outside all of those, the process also uses native memory for thread stacks (thread count × -Xss), direct buffers (ByteBuffer.allocateDirect, used by NIO and libraries like Netty; capped by -XX:MaxDirectMemorySize, which defaults to the max heap size), memory-mapped files, the garbage collector's own data structures, symbol tables and the JIT compilers' working memory. That's why a JVM with -Xmx2g can easily use 3 GB of RAM, and why a container memory limit must leave room above -Xmx. Native Memory Tracking (-XX:NativeMemoryTracking=summary plus jcmd <pid> VM.native_memory summary) shows the breakdown.

  6. 6

    Inside the heap, objects have a header before their fields. On 64-bit HotSpot it's a mark word (8 bytes: hash code, lock state, GC age) plus a class pointer (4 bytes with compressed class pointers), and arrays add a 4-byte length; every object is padded to a multiple of 8 bytes. So new Object() takes 16 bytes and an Integer 16 bytes to hold 4 bytes of data. Compressed oops store references as 32-bit values for heaps under about 32 GB, which is why a 31 GB heap can hold more objects than a 33 GB one. New objects are allocated by bumping a pointer inside the thread's private TLAB (thread-local allocation buffer), which makes new about as cheap as a few machine instructions.

Explain it without notes

01

Name the JVM's memory areas and what lives in each.

02

Your container keeps getting OOMKilled but the Java logs show no OutOfMemoryError. Why, and what do you do?

03

What changed when Metaspace replaced PermGen in Java 8?

04

Why does a HashMap<Integer, Integer> with a million entries use so much more memory than two int[] arrays of a million?

Practice

01

Write a program that recurses until StackOverflowError, catches it, and prints whether a method with more local variables reached a smaller depth than one with fewer (print only the boolean).

02

Show that Integer.valueOf(100) returns the same object every time but new Integer(100)-style allocation (use Integer.valueOf on 1000) does not.

03

Write a program that creates a direct buffer, writes the ints 1 to 4 into it and reads them back, printing the sum.

Trade-offs

  • ↔

    A larger heap reduces GC frequency but takes memory from everything else on the machine, and above about 32 GB loses compressed oops.

  • ↔

    Direct buffers avoid a copy for I/O but are slower to allocate, are freed only when their small heap object is collected, and are invisible to heap-size tuning.

  • ↔

    Smaller thread stacks (-Xss) let you run more platform threads but make deep recursion fail sooner; virtual threads (Topic 13.10) sidestep the trade-off by storing their stacks on the heap.

Done when you can

  • Done when you can draw the JVM memory map and say which areas are per-thread and which are shared.

  • Done when you can read an OutOfMemoryError message and name the area and the likely fix.

  • Done when you can estimate an object's size from its header and fields.

  • Done when you can size a JVM for a container, leaving room for non-heap memory.

  • Done when you can use Native Memory Tracking to see where non-heap memory goes.