Topic 3.11
Stack and Heap: Where Your Data Lives
In one line
A running Java program keeps method calls and their local variables on a per-thread stack, and all objects and arrays on a shared heap. Locals hold primitive values or references; the objects those references point to live on the heap until the garbage collector reclaims them.
Think of it like this
A restaurant. Each waiter carries a small notepad (the stack) with the current order on top; when an order is done, they tear off that page. The kitchen has one big shared fridge (the heap) where all the food lives. The notepad doesn't hold the food, just notes like "the cake on shelf 3" (a reference). When nobody's notepad mentions a dish any more, the cleaner (the garbage collector) clears it out.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Stack
- A per-thread area of memory holding one frame for each method call in progress. Last in, first out.
- Stack frame
- The block of stack memory for one method call: its parameters, locals and working space. Removed when the call returns.
- Heap
- The shared area of memory where all objects and arrays live.
- Reference
- A value stored in a variable that leads to an object on the heap. Several references can lead to the same object.
- Garbage collector (GC)
- The part of the JVM that finds objects no longer reachable and frees their memory automatically.
- Reachable
- An object is reachable if a chain of references leads to it from a live local variable, a static field, or similar roots. Only unreachable objects are collected.
- Thread
- An independent path of execution inside a program. Each thread has its own stack; all threads share the heap (Phase 13).
- `-Xss` / `-Xmx`
- JVM options that set the stack size per thread and the maximum heap size, for example
-Xss2mand-Xmx512m.
Step by step
01Two kinds of memory, two jobs
The stack and the heap solve different problems. The stack is for things whose lifetime matches a method call exactly: parameters and local variables. It grows and shrinks with calls, and freeing a frame is just moving a pointer.
The heap is for things whose lifetime isn't tied to one call: objects that are returned, stored in fields or shared. Those need a smarter cleanup process, the garbage collector.
02A snapshot in the middle of a call
Follow this program: main sets int n = 3 and calls makeScores(n), which creates int[] scores = new int[count] and fills it.
While makeScores is running, there are two frames on main's stack. main's frame holds n = 3 (a value) and result, not yet assigned. makeScores' frame holds count = 3 (a copy of n), i, and scores, a reference to the array on the heap.
static int[] makeScores(int count) {
int[] scores = new int[count]; // the array is on the heap
for (int i = 0; i < count; i++) {
scores[i] = (i + 1) * 10;
}
return scores; // returns the reference
}
public static void main(String[] args) {
int n = 3;
int[] result = makeScores(n);
}03After the return: the frame is gone, the array survives
return scores; hands the reference back to main, which stores it in result. Then makeScores' frame is popped: count, i and scores no longer exist.
The array is untouched. It was never in the frame; only a reference to it was. Now result in main is the only reference to it, so it stays alive.
This is how methods build and return objects in Java, and why there's no "dangling pointer" problem like returning the address of a local in C: locals are never on the heap, and objects are never in frames.
04Garbage: when nothing points to an object
If main now does result = null;, no reference leads to the array any more. It's unreachable, so it's garbage. The GC will reclaim the memory at some point; you can't predict exactly when, and you don't need to.
The GC starts from roots (local variables in every live frame of every thread, static fields, and a few JVM internals) and marks everything reachable from them. Whatever isn't marked is free space. That's why a stack frame's locals matter to the GC: they're roots.
Calling System.gc() is only a request. Real tuning of the heap and collectors is Topic 14.3.
05What exactly lives where
On the stack (in frames): parameters and local variables, whether they're primitive values (int n = 3) or references (int[] result). Also each call's return address and temporary working values.
On the heap: every object and array, including all their fields (so an object's int field is on the heap, inside the object), String objects (string literals are kept in the string pool, which is part of the heap since Java 7, Topic 6.1), and boxed values like Integer.
Elsewhere: class metadata (the description of each class and its methods) lives in Metaspace, native memory outside the heap since Java 8. In HotSpot, the values of static fields are stored with the class's Class object on the heap (Topic 14.2).
06Stack vs heap at a glance
Owner: stack is per thread; heap is shared by all threads. Lifetime: stack data dies when its method returns; heap data lives while reachable. Cleanup: the stack pops automatically; the heap is cleaned by the GC. Speed: both are fast to allocate in HotSpot (heap allocation is usually a pointer bump in a thread-local buffer), but the heap has GC costs later.
Size: stacks are small (about 1 MB per thread by default on 64-bit systems); the heap is large (by default up to a quarter of physical memory, and set with -Xmx). Errors: StackOverflowError vs OutOfMemoryError.
Because each thread has a private stack, local variables are never shared between threads, and are therefore thread-safe by nature. Objects on the heap can be reached by many threads at once, which is where all the problems of Phase 13 (Concurrency) come from.
07Running out of each
A stack runs out with deep recursion: java.lang.StackOverflowError. A heap runs out when reachable objects exceed -Xmx: java.lang.OutOfMemoryError: Java heap space. A single impossibly large array gives OutOfMemoryError: Requested array size exceeds VM limit.
The two errors point to different fixes. Stack overflow: reduce recursion depth (loop instead) or, as a stopgap, raise -Xss. Heap exhaustion: find what keeps objects reachable (a memory leak, often a collection that only grows) with a heap dump (Topic 14.5), or raise -Xmx if the data really needs it.
Try it yourself
- 1
Draw it, then run it
Before running "A frame dies, its array lives on", draw the stack and heap at the moment
same[0] = 99;runs: which frames exist, which variables hold values and which hold references, and how many arrays are on the heap. Then run and check that the output matches your drawing. - 2
Change the stack size
Copy "Catching a stack overflow" into
Main.javaand run it with different stack sizes. The depth roughly scales with-Xss. The exact numbers will differ on your machine.terminal$ java -Xss256k Mainjava -Xss4m Main── expected output ──stack overflowed after about 2484 framesstack overflowed after about 75023 framesExample numbers only. They vary by JVM version, platform and whether the method was JIT-compiled. - 3
Run out of heap on purpose
Write a program that allocates
new long[100_000_000](800 MB) and run it withjava -Xmx32m Main, then with-Xmx1g. The first fails withJava heap space; the second prints normally.
Code & diagrams
Expected output
result = [10, 20, 30]
result[0] = 99
same object? true
n = 3, m = 100Each recursive frame keeps its own local; all of them write into the same heap array.
Expected output
frame for index 3 still has local = 9
frame for index 2 still has local = 4
frame for index 1 still has local = 1
frame for index 0 still has local = 0
heap array: [0, 1, 4, 9]Expected output
x == y: true
a == b: false
s1 == s2: true
s1 == s3: false
s1.equals(s3): trueNot marked runnable: how deep it gets depends on the JVM, the stack size and the JIT. Never rely on catching StackOverflowError in real code.
public class Main {
static int depth = 0;
static void dive() {
depth++;
dive();
}
public static void main(String[] args) {
try {
dive();
} catch (StackOverflowError e) {
System.out.println("stack overflowed after about " + depth + " frames");
}
}
}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
Allocate more than the heap allows
Create an 800 MB array with a 32 MB heap limit.
public class Main {
public static void main(String[] args) {
long[] big = new long[100_000_000];
System.out.println(big.length);
}
}Break #2
Ask for an impossible array
Request new long[Integer.MAX_VALUE].
public class Main {
public static void main(String[] args) {
long[] big = new long[Integer.MAX_VALUE];
}
}Myth vs fact
Myth
Primitives live on the stack and objects live on the heap.
Fact
Local primitives live in frames, but primitive fields live inside their objects on the heap, and array elements live in the array on the heap.
Myth
Objects created inside a method are destroyed when the method returns.
Fact
Only the method's locals are removed. The objects stay on the heap while anything still references them, and the GC reclaims them later.
Myth
Setting a variable to null frees the object immediately.
Fact
It only removes one reference. The object is freed by the GC some time after it becomes unreachable, and usually you don't need null assignments at all.
Myth
The stack is fast and the heap is slow.
Fact
Heap allocation in HotSpot is usually a pointer bump in a thread-local buffer, nearly as cheap as stack allocation. The cost of the heap comes later, as garbage collection work.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Each frame in HotSpot has a local variable array (slots numbered as in
iload_1), an operand stack for intermediate values, and a link to the method's constant pool.javap -vshowsstack=andlocals=for every method: the maximum sizes the verifier checked at class-load time. - ▸
Escape analysis in the C2 JIT can prove that an object never escapes a method; it may then replace the object with plain local values (scalar replacement), so that particular
newnever touches the heap at all. "Objects always live on the heap" is the language model; the JIT may optimise beneath it. - ▸
Heap allocation uses TLABs (thread-local allocation buffers): each thread gets a private chunk of the young generation and allocates by bumping a pointer, with no locking. That's why allocating short-lived objects is cheap in Java and why generational GC works so well (Topic 14.3).
- ▸
Default sizes: the maximum heap defaults to 1/4 of physical memory (container-aware since JDK 10), and on 64-bit Linux and macOS the default thread stack is 1 MB. Virtual threads (Java 21, Topic 13.10) keep their stacks on the heap as resizable chunks, which is how millions of them fit in memory.
Remember this
- 1
Every thread has its own stack. Each method call pushes a stack frame holding that call's parameters, local variables and some working space; returning pops it. Frames are created and destroyed in strict last-in, first-out order, which makes the stack very fast and needs no cleanup.
- 2
The heap is one big area shared by all threads, where every object and array is created with
new(and where string literals and boxed values live too). Objects don't belong to any frame, so they can outlive the method that created them. - 3
A local variable of a primitive type stores the value in the frame. A local of an object or array type stores a reference in the frame, and the object itself is on the heap. Fields are stored inside their object, so an
intfield lives on the heap, with its object. - 4
When a frame is popped, its references disappear. An object that is no longer reachable from any live reference (from any frame, static field, or other reachable object) becomes garbage, and the garbage collector (GC) frees its memory at some later time. You never free memory yourself in Java.
- 5
Each region has its own failure: running out of stack (usually deep recursion) throws
StackOverflowError(Topic 3.7); running out of heap throwsOutOfMemoryError: Java heap space. Their sizes are set with-Xss(stack per thread) and-Xmx(maximum heap). - 6
This picture explains everything in this phase: pass-by-value copies the frame's value (Topic 3.3), locals die with their frame (Topic 3.6), recursion stacks frames (Topic 3.7), and arrays are heap objects behind references (Topic 3.8). Topic 14.2 goes deeper into all JVM memory areas.
Explain it without notes
What is stored on the stack and what on the heap in a running Java program?
When makeScores returns an array it created, why is the array still usable by the caller?
When does an object become eligible for garbage collection?
What's the difference between StackOverflowError and OutOfMemoryError: Java heap space, and how do you fix each?
Why are local variables thread-safe while objects may not be?
Practice
Write a program where a method makeGreeting(String name) builds and returns a String, and main prints it. In comments, mark which variables are in which frame and which objects are on the heap.
Predict the output, then run: int[] a = {1, 2}; int[] b = a; a = new int[] {9, 9}; System.out.println(b[0] + " " + a[0]);
Write a method static int[] counter() that keeps a running count across calls by storing it in an array held in a static field, and call it three times, printing the count each time.
Trade-offs
- ↔
Raising
-Xssallows deeper recursion but costs memory for every thread; with thousands of platform threads it adds up quickly. Fix depth in code first. - ↔
A bigger heap (
-Xmx) avoidsOutOfMemoryErrorand can reduce GC frequency, but takes memory from other processes and can lengthen some GC pauses. Size it from measurements, not guesses. - ↔
Holding references to objects you no longer need (in static fields, caches or long-lived collections) keeps them reachable, which is how Java programs leak memory despite having a garbage collector.
Done when you can
Done when you can draw the stack frames and heap objects for a small program at any line.
Done when you can say exactly where a local primitive, a local reference, an object's field and an array element are stored.
Done when you can explain why an object outlives the method that created it.
Done when you can explain reachability and when an object becomes garbage.
Done when you can tell
StackOverflowErrorfromOutOfMemoryErrorand choose the right fix.