Heap, Stack and Metaspace in Java

Where each piece of data actually lives, and what runs out when each area is exhausted.

The three areas that matter

StackHeapMetaspace
HoldsFrames, locals, referencesObjects and arraysClass metadata, static fields
SharedOne per threadAll threadsAll threads
Managed byAutomatic push and popThe garbage collectorFreed when a loader is collected
Sized with-Xss-Xms and -Xmx-XX:MaxMetaspaceSize
ExhaustionStackOverflowErrorOutOfMemoryError: Java heap spaceOutOfMemoryError: Metaspace

Following one method

public class Demo {

    private static final String APP = "notes";        // metaspace, object on heap

    public void run() {
        int count = 5;                                 // stack
        String label = "total";                        // reference stack, object heap
        int[] values = new int[3];                     // reference stack, array heap
        Note note = new Note(label, count);            // reference stack, object heap
    }
}
Thread stack                     Heap
+-------------------+            +--------------------------+
| run() frame       |            | "total"  (string pool)   |
|  count  = 5       |            | int[3]   {0,0,0}         |
|  label  --------------------> | Note{title, views}       |
|  values --------------------> +--------------------------+
|  note   -------------------->
+-------------------+

Metaspace: Demo class structure, method bytecode, the APP field slot

The stack

public static int depth(int n) {
    return depth(n + 1);        // no base case
}
// StackOverflowError after tens of thousands of frames
  • One stack per thread, typically half a megabyte to a megabyte.
  • A frame is pushed on every call and popped on return.
  • Nothing is garbage collected; the memory is reclaimed by the pop.
  • Thread confined, so stack data needs no synchronisation.

Note that thousands of threads consume real memory in stacks alone, which is one of the reasons virtual threads exist.

The heap

+-------------------------------------------------------+
| Young generation                  | Old generation      |
| Eden        | S0      | S1        | tenured objects     |
+-------------------------------------------------------+
  • Every object and array is allocated here.
  • New objects go into Eden.
  • A minor collection copies survivors between the survivor spaces.
  • Objects surviving enough collections are promoted to the old generation.

This split exists because of a strong empirical observation: most objects die very young. Collecting a small young generation frequently is far cheaper than scanning the whole heap.

java -Xms1g -Xmx4g -XX:+UseG1GC -jar app.jar

Metaspace

Before Java 8 class metadata lived in a fixed size area called PermGen, which overflowed easily in applications that loaded many classes. Metaspace replaced it and is allocated from native memory, growing as needed unless capped.

java -XX:MaxMetaspaceSize=256m -jar app.jar

Metaspace is reclaimed only when an entire class loader becomes unreachable. Repeated redeployment in a container is the classic cause of a metaspace leak.

Where a String lives

String a = "java";                    // pooled, on the heap
String b = "java";                    // the same pooled object
String c = new String("java");        // a separate heap object
String d = c.intern();                // the pooled instance

System.out.println(a == b);           // true
System.out.println(a == c);           // false
System.out.println(a == d);           // true

The string pool has lived on the normal heap since Java 7, so pooled strings are collected like anything else.

Escape analysis

public int total(int a, int b) {
    Point point = new Point(a, b);    // may never be allocated at all
    return point.x() + point.y();
}

If the JIT can prove an object never escapes the method, it may replace it with plain local values. This is a real optimisation, but it depends on inlining and cannot be relied upon; write clear code and let the JVM decide.

Diagnosing memory

jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jmap -dump:live,format=b,file=heap.hprof <pid>
jstat -gc <pid> 1s
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dumps -jar app.jar

Enable the heap dump flag in production. An out of memory error without a dump is very difficult to explain after the fact.

Reading the errors

MessageUsual cause
Java heap spaceA genuine leak, or a heap too small for the workload
GC overhead limit exceededCollection is running constantly and reclaiming almost nothing
MetaspaceToo many classes, or a class loader leak
unable to create native threadToo many threads, or stacks too large
Requested array size exceeds VM limitAn array larger than the implementation allows

Common misconceptions

  • Primitives always live on the stack. A primitive field lives inside its object on the heap.
  • Setting a reference to null frees memory. It removes one reference; collection happens later, if at all.
  • A bigger heap is always better. More heap can mean longer pauses.
  • The stack is garbage collected. It is not; frames are popped.

Practice

  1. For a method with a local int, a String and an array, state where each part lives.
  2. Why does creating thousands of platform threads consume so much memory?
  3. Explain why the heap is split into generations.
  4. Trigger a StackOverflowError and an OutOfMemoryError deliberately and compare the messages.
  5. What condition would cause metaspace to grow without bound?

Conclusion

Frames and references live on per thread stacks, objects live on the shared heap, and class metadata lives in metaspace. Knowing which area an error names is the first step towards diagnosing it.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.