Java Memory Structure

Learning goals

By the end of this chapter you should be able to:

  • Name JVM memory regions: heap, stack, metaspace, code cache
  • Answer “where does this variable live?” for locals, fields, statics, and arrays
  • Connect object references to the heap layout you use when choosing collections
  • Relate memory regions to OutOfMemoryError and StackOverflowError

From your code to JVM layout

You already write new ArrayList<>(), store User references in Map, and pass primitives to methods. Each of those choices implies objects on the heap and references on the stack.

void register(User user) {          // 'user' reference → stack frame
    List<String> roles = new ArrayList<>();  // ref stack, list object heap
    roles.add("ADMIN");
}

This chapter maps that mental model to the official JVM regions: what is shared vs per-thread, what the GC manages, and what fails when a region fills up.

The detailed breakdown below builds on notes you have already written about objects and references — read it as the full runtime picture behind every collection and new you use in application code.



Overview

When the JVM starts, it divides memory into well-defined regions: heap, stack, metaspace, code cache, and a few smaller ones. Each region holds different things and is managed differently — knowing which is which is the foundation for understanding garbage collection, OutOfMemoryError, and StackOverflowError.


Table of contents

  1. The big picture
  2. Heap — where objects live
  3. Stack — per-thread method frames
  4. Metaspace — class metadata
  5. Code cache — JIT-compiled code
  6. PC register & native method stack
  7. Heap vs Stack — where does this variable live?
  8. Common memory errors
  9. Interview questions
  10. Summary

1. The big picture

+-------------------- JVM Process --------------------+
|                                                     |
|   +---------------- Shared across threads --------+ |
|   |  Heap                                         | |
|   |   ├─ Young Generation (Eden + Survivors)      | |
|   |   └─ Old (Tenured) Generation                 | |
|   |  Metaspace          (class metadata)          | |
|   |  Code Cache         (JIT compiled methods)    | |
|   +-----------------------------------------------+ |
|                                                     |
|   +---------------- Per thread ------------------+   |
|   |  Stack              (method frames)          |   |
|   |  PC Register        (current instruction)    |   |
|   |  Native Method Stack (for JNI calls)         |   |
|   +----------------------------------------------+   |
+-----------------------------------------------------+

Two camps:

  • Shared regions live for the lifetime of the JVM (or longer for some). Multiple threads access them.
  • Per-thread regions are created when a thread starts and destroyed when it ends.

2. Heap — where objects live

The heap is the big shared area where every object created with new lives. It’s also the area the garbage collector manages.

String s = new String("hello");   // the String object is on the heap
List<Integer> list = new ArrayList<>();  // the list and its array → heap

The heap is split into generations for efficient GC:

  • Young generation — Eden + two Survivor spaces. New objects start here. Most die young.
  • Old (Tenured) generation — objects that survived several GCs get promoted here.

Sized with:

-Xms512m              # initial heap size
-Xmx2g                # maximum heap size
-XX:MaxRAMPercentage=75   # use 75% of container memory (better in Docker/K8s)

When the heap is full and the GC can’t free space → OutOfMemoryError: Java heap space.

Deep dive on how the GC manages this area: 02-garbage-collection.md.


3. Stack — per-thread method frames

Every thread has its own stack. The stack holds one frame per active method call.

A frame contains:

  • The method’s local variables.
  • The operand stack (for intermediate values during bytecode execution).
  • A reference back to the method’s class.
void outer() {
    int x = 10;          // x is on outer's frame
    inner(x);            // pushes a new frame for inner
}

void inner(int n) {
    int doubled = n * 2; // doubled is on inner's frame
}                        // inner's frame popped here

Key properties:

  • Fast — push/pop on a frame, no GC needed.
  • Small — usually 256 KB to 1 MB per thread. Set with -Xss512k.
  • Frames are automatically freed when the method returns.
  • Deep or infinite recursion → StackOverflowError.

Important: the stack holds primitives (int, long, etc.) and object references. The objects themselves still live on the heap.

void example() {
    int n = 5;                    // n itself is on the stack
    String s = new String("hi");  // s is a reference on the stack; "hi" object is on the heap
}

4. Metaspace — class metadata

Metaspace holds info about classes: method bytecode, field layouts, static fields, runtime constant pool.

  • Introduced in Java 8, replacing the older fixed-size PermGen.
  • Lives in native memory (outside the heap).
  • Grows automatically as you load classes. Cap it with -XX:MaxMetaspaceSize=256m.
  • Filling it up → OutOfMemoryError: Metaspace — usually caused by classloader leaks (loading the same classes over and over without unloading).

5. Code cache — JIT-compiled code

The JVM starts by interpreting bytecode. After a method runs many times, the JIT compiler (HotSpot has C1 and C2) compiles it to native machine code for speed. That compiled code lives in the code cache.

  • Size: -XX:ReservedCodeCacheSize=240m.
  • If full, the JIT stops compiling new methods → app slows down (rare, but possible in very long-running apps).

6. PC register & native method stack

Two smaller per-thread regions that you mention by name in interviews:

  • PC register — holds the address of the current JVM instruction being executed by that thread.
  • Native method stack — used when Java calls into native code (JNI), e.g. native libraries.

7. Heap vs Stack — where does this variable live?

Variable Lives on
int n = 5 (local in a method) Stack
String s = new String(...) (local) Reference on stack, object on heap
Method parameter Stack (same rules)
Instance field of an object Heap (inside the owning object)
static field Heap (was Metaspace before Java 8)
Array (new int[100]) Heap
Class metadata, bytecode Metaspace

Rule: the variable name lives on the stack; the object it points to lives on the heap.


8. Common memory errors

Error Cause Fix
OutOfMemoryError: Java heap space Heap full, GC can’t free anything Larger -Xmx, or fix memory leak
OutOfMemoryError: Metaspace Too many classes loaded Cap MaxMetaspaceSize, find classloader leak
OutOfMemoryError: GC overhead limit exceeded GC running constantly, freeing <2% per run Heap too small or leak
OutOfMemoryError: unable to create new native thread OS thread limit hit Reduce threads, use virtual threads (Java 21+)
StackOverflowError Recursion too deep Convert to iteration, or grow -Xss

9. Interview questions

Q: What’s the difference between heap and stack? A: Stack is per-thread, holds local variables and method frames, and is freed automatically when methods return. Heap is shared, holds all objects created with new, and is managed by the garbage collector.

Q: Where do primitives and references live? A: Local primitives and references live on the stack (inside the method’s frame). The objects those references point to live on the heap.

Q: What is Metaspace? What did it replace? A: Metaspace holds class metadata (bytecode, field layouts, runtime constant pool). It replaced PermGen in Java 8. Unlike PermGen, it grows automatically and lives in native memory, not the heap.

Q: Where do static fields live? A: On the heap (since Java 8). Before Java 8 they were in PermGen.

Q: What causes StackOverflowError? A: A thread’s stack ran out of space — almost always deep or infinite recursion. Grow with -Xss or switch to iteration.

Q: What’s the difference between OutOfMemoryError: Java heap space and Metaspace? A: Heap space — too many objects, often a leak in your code (long-lived collections, listeners). Metaspace — too many classes, usually a classloader leak in apps that redeploy without restarting (old app servers).

Q: What is the code cache? A: Where the JIT compiler stores native code compiled from hot methods. Sized with -XX:ReservedCodeCacheSize.

Q: Are arrays stored on the heap or stack? A: Arrays are objects, so they’re always on the heap. The reference to them lives on the stack if declared as a local variable.

Q: How is memory allocated to threads? A: Each thread gets its own stack (~512 KB default), PC register, and native method stack. All threads share the heap, metaspace, and code cache.


10. Summary

Region Shared / per-thread Holds Failure
Heap Shared All objects, arrays, static fields OOM: Java heap space
Stack Per-thread Local variables, method frames StackOverflowError
Metaspace Shared (native) Class metadata OOM: Metaspace
Code Cache Shared (native) JIT-compiled code JIT stops compiling
PC Register Per-thread Current instruction pointer
Native Stack Per-thread JNI call frames OS-level limit

Mental model: the stack holds the name; the heap holds the thing.


Next steps


Happy Learning


Practice checkpoint

  1. Local int n = 5 inside a method — stack or heap? Answer: n lives in the method’s stack frame.

  2. Instance field String name on a User object — where? Answer: inside the User object on the heap.

  3. static Map<String, Config> CACHE — where after Java 8? Answer: static field storage on the heap (Class object / related metadata in metaspace).

  4. Deep recursion with no base case — which error first? Answer: StackOverflowError (thread stack exhausted).


Interview angles

  • Heap vs stack: shared GC-managed objects vs per-thread frames freed on return.
  • Metaspace vs PermGen: class metadata in native memory; grows automatically since Java 8.
  • Reference vs object: stack holds reference value; new allocates on heap.
  • Tuning entry points: -Xmx, -Xss, -XX:MaxMetaspaceSize — know symptom → flag direction.

Next: /java/learn/jvm/garbage-collection/