Why this matters
StackOverflowErrorandOutOfMemoryErrorare both memory failures, and the one you get tells you exactly which region filled up.- Thread safety only exists as a concern because stacks are private and the heap is shared.
- Every later topic — garbage collection, memory leaks, escape analysis — assumes you can place a given value in the right region.
The two regions
Popped the moment a method returns.
Cleared by the garbage collector, not by returning.
| Aspect | Stack | Heap |
|---|---|---|
| Holds | Method frames: locals, parameters, references, return address | Every object and array, plus their instance fields |
| Shared | One private stack per thread | One heap shared by all threads |
| Allocation | Push a frame on call, pop on return | Allocated by new, reclaimed by the collector |
| Size | Small — around 512 KB to 1 MB per thread | Large — hundreds of megabytes to gigabytes |
| Lifetime | Exactly the duration of the method call | Until no reference can reach the object |
| Speed | Very fast — a pointer bump | Slower, and subject to collection pauses |
| When exhausted | StackOverflowError | OutOfMemoryError |
Holds
StackMethod frames: locals, parameters, references, return addressHeapEvery object and array, plus their instance fieldsShared
StackOne private stack per threadHeapOne heap shared by all threadsAllocation
StackPush a frame on call, pop on returnHeapAllocated by new, reclaimed by the collectorSize
StackSmall — around 512 KB to 1 MB per threadHeapLarge — hundreds of megabytes to gigabytesLifetime
StackExactly the duration of the method callHeapUntil no reference can reach the objectSpeed
StackVery fast — a pointer bumpHeapSlower, and subject to collection pausesWhen exhausted
StackStackOverflowErrorHeapOutOfMemoryError
Private and automatic versus shared and collected. Those two differences explain everything else.
Placing a value
public class Demo {
private int instanceField = 10; // heap, inside the Demo object
private static int staticField = 20; // heap, with the class metadata
public void process(int parameter) { // stack — a copy of the argument
int local = 5; // stack — the value itself
String text = "hello"; // stack holds the reference; the String is in the pool
int[] numbers = new int[3]; // stack holds the reference; the array is on the heap
Demo other = new Demo(); // stack holds the reference; the object is on the heap
} // the whole frame is popped here
}
The rule, stated precisely
- A primitive local stores its value directly in the frame.
- A reference local stores an address in the frame; the object it names is on the heap.
- Instance fields live inside the heap object, whatever their type — an
intfield is on the heap. - Static fields live with the class, in metaspace, and are reachable from every thread.
- The frame disappears the moment the method returns. The objects it referenced survive if anything else still reaches them.
Frames and the call chain
Each method call pushes a frame. Each return pops one. The chain of frames is precisely what a stack trace prints.
void a() { b(); }
void b() { c(); }
void c() { throw new IllegalStateException("here"); }
// Stack when the exception is thrown, top first:
// c() ← throw site
// b()
// a()
// main()
Unbounded recursion never pops, so the stack fills:
static int recurse(int n) {
return recurse(n + 1); // StackOverflowError after a few thousand frames
}
The depth you reach depends on the frame size and the thread's stack size, set with -Xss. Raising it
delays the error; it does not fix a missing base case.
Why thread safety exists
public class Counter {
private int count = 0; // heap — shared by every thread
public void increment() {
int local = count; // stack — private to this thread
local = local + 1;
count = local; // writes back to shared memory
}
}
Two threads each have their own local. Both may read count as 5, compute 6, and write 6. One increment
is lost. The locals were never the problem; the shared heap field was.
This is the reason a method using only locals is automatically thread-safe, and why anything touching instance or static state needs thought.
Escape analysis
The JIT compiler can sometimes prove an object never leaves the method that created it, and then allocate its fields directly in the frame — scalar replacement — avoiding the heap entirely.
public double distance(double x1, double y1, double x2, double y2) {
Point from = new Point(x1, y1); // may never reach the heap at all
Point to = new Point(x2, y2);
return from.distanceTo(to);
}
Because neither Point is stored, returned or passed to unknown code, the JIT may eliminate both
allocations. This is why writing small short-lived helper objects for clarity is usually free in practice.
Reading the two errors
| Aspect | Error | What it means and where to look |
|---|---|---|
| StackOverflowError | A thread's stack is full | Unbounded recursion, or mutually recursive calls |
| OutOfMemoryError: Java heap space | Too many live objects | A leak, an unbounded cache, or a genuinely undersized heap |
| OutOfMemoryError: Metaspace | Too many loaded classes | Repeated redeployment, or heavy dynamic proxy generation |
| OutOfMemoryError: unable to create native thread | The OS refused another thread | Thousands of threads; each needs its own stack |
StackOverflowError
ErrorA thread's stack is fullWhat it means and where to lookUnbounded recursion, or mutually recursive callsOutOfMemoryError: Java heap space
ErrorToo many live objectsWhat it means and where to lookA leak, an unbounded cache, or a genuinely undersized heapOutOfMemoryError: Metaspace
ErrorToo many loaded classesWhat it means and where to lookRepeated redeployment, or heavy dynamic proxy generationOutOfMemoryError: unable to create native thread
ErrorThe OS refused another threadWhat it means and where to lookThousands of threads; each needs its own stack
The message after the colon is the most useful part. Read it before changing any flag.
The last row connects back to the first column: each thread needs its own stack, so thread count and stack size multiply. Ten thousand threads at 1 MB each is 10 GB of stack memory before a single object is allocated — which is precisely the constraint virtual threads were designed to remove.
Common misreadings
- "Primitives are always on the stack." A primitive local is. An
intinstance field lives on the heap inside its object. - "The reference and the object are in the same place." The reference is in the frame; the object is on the heap. This is the whole distinction.
- "Each thread gets its own heap." Only stacks are per-thread. One shared heap is why synchronisation exists.
- "Strings are on the stack because they look like values."
Stringis an object. The pool is a region of the heap. - "Raising
-Xssfixes aStackOverflowError." It delays it. Fix the recursion. - "
OutOfMemoryErroralways means a leak." It may mean an undersized heap or a legitimately large working set. The heap dump tells you which.
Quick recall
Everything you need if you only revisit this box.
- Stack = per-thread frames holding locals, parameters and references. Heap = shared, holds every object and array.
- A reference local lives on the stack; the object it names lives on the heap. Instance fields live inside the heap object.
- Frames are pushed on call and popped on return, which is exactly what a stack trace shows.
StackOverflowErrormeans runaway recursion;OutOfMemoryErrormeans too many live objects.- Thread safety is a concern only because the heap is shared — code using just locals is inherently safe.
- Escape analysis may keep short-lived objects off the heap, which makes small helper objects effectively free — but do not design around it.
- Every thread needs its own stack, so thread count times stack size is real memory.
Test yourself
Answer these before moving on — recall is what makes it stick.