Skip to content
  • JVM Memory: Heap, Stack & Metaspace · Java

JVM Memory: Heap, Stack & Metaspace

LearnViaNotes @learnvianotes· 5 min read

The JVM splits memory into areas with different lifetimes: objects on the heap, method calls on per-thread stacks, class metadata in metaspace.

The runtime data areas

AreaShared?HoldsError when full
Heap — young gen (Eden + 2 Survivor spaces)All threadsNewly created objects and arraysOutOfMemoryError: Java heap space
Heap — old (tenured) genAll threadsObjects that survived several young GCsOutOfMemoryError: Java heap space
JVM stack (one per thread)Per threadOne frame per method call: local variables, operand stack, return addressStackOverflowError
Metaspace (native memory, Java 8+)All threadsClass metadata: method bytecode, field layout, runtime constant poolOutOfMemoryError: Metaspace
PC registerPer threadAddress of the bytecode instruction being executed—
Native method stackPer threadFrames of native (C/C++) methods called through JNIStackOverflowError
Code cacheAll threadsMachine code produced by the JIT compilerJIT stops compiling (warning)

What lives where

Java
public class Main {
    static class Box { int value; }

    public static void main(String[] args) {
        int a = 10;           // primitive local → in main's stack frame
        int b = a;            // copies the value
        b++;

        Box x = new Box();    // reference x on the stack, Box object on the heap
        Box y = x;            // copies the reference, not the object
        y.value = 42;

        System.out.println(a + " " + b);
        System.out.println(x.value + " " + (x == y));
    }
}
Output
10 11
42 true

Stack frames and StackOverflowError

  1. Call

    Calling a method pushes a new frame onto the current thread's stack.

  2. Execute

    The frame holds the method's parameters and locals, plus an operand stack the bytecode works on.

  3. Return

    The frame is popped — its locals disappear instantly. No GC is needed for stack memory.

Java
public class Main {
    static int depth = 0;

    static void recurse() {
        depth++;
        recurse();              // no base case → a new frame on every call
    }

    public static void main(String[] args) {
        try {
            recurse();
        } catch (StackOverflowError e) {
            System.out.println("StackOverflowError after more than 1000 frames: " + (depth > 1000));
        }
    }
}
Output
StackOverflowError after more than 1000 frames: true

OutOfMemoryError

Run with a deliberately tiny heap (-Xmx64m) and ask for a 400 MB array:

Java
public class Main {
    public static void main(String[] args) {
        try {
            int[] huge = new int[100_000_000];      // ~400 MB
            System.out.println(huge.length);
        } catch (OutOfMemoryError e) {
            System.out.println("OutOfMemoryError: " + e.getMessage());
        }
    }
}
Output
OutOfMemoryError: Java heap space
StackOverflowErrorOutOfMemoryError
AreaA thread's stackHeap, metaspace, direct buffers, native threads…
Typical causeUnbounded / too deep recursionLeak (objects stay reachable), or heap simply too small
FixBase case, iteration, or bigger -XssFix the leak (heap dump), then size -Xmx
TypeBoth extend Error (via VirtualMachineError) — not meant to be caught in normal code

Sizing flags

FlagMeaningExample
-XmsInitial heap size-Xms512m
-XmxMaximum heap size (default: 1/4 of physical RAM)-Xmx2g
-XssStack size per thread-Xss1m
-XX:MaxMetaspaceSizeCap metaspace (unbounded by default)-XX:MaxMetaspaceSize=256m
-XX:+HeapDumpOnOutOfMemoryErrorWrite a .hprof heap dump when OOM happenswith -XX:HeapDumpPath=/tmp
Terminal
$ java -Xms256m -Xmx1g -Xss512k -XX:+HeapDumpOnOutOfMemoryError App.java
$ java -XX:+PrintFlagsFinal -version | grep -E "MaxHeapSize|ThreadStackSize"

Escape analysis

The JIT checks whether an object escapes the method that creates it (returned, stored in a field, passed to another thread). If it doesn't, the JIT may scalar-replace it — keep its fields in registers/stack slots and never allocate it on the heap at all.

Java
public class Main {
    record Point(int x, int y) {}

    static int distanceSquared(int x, int y) {
        Point p = new Point(x, y);      // never escapes → candidate for scalar replacement
        return p.x() * p.x() + p.y() * p.y();
    }

    public static void main(String[] args) {
        long sum = 0;
        for (int i = 0; i < 1_000; i++) sum += distanceSquared(i, i);
        System.out.println(sum);
    }
}
Output
665667000

Where are local variables, objects and static variables stored?

What replaced PermGen and why?

Is the stack or the heap thread-safe?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report