Java Stack Memory
Table of Contents + −
In the last lesson you learned about Java heap memory, where your objects live. But something also has to track which method is running right now and where to go back when it finishes. That job belongs to the stack.
🤔 The problem the stack solves
When one method calls another, which calls another, the JVM has to remember a lot:
- The local variables of every call still in progress.
- The return spot for each one, so it can jump back.
- The right order, so the most recent call comes back first.
The call stack is that memory of “what am I in the middle of doing.”
🧠 A frame for every method call
The stack works like a stack of plates: add on top, take from the top. That rule is LIFO, Last In, First Out. Each method call gets one block of memory on top called a stack frame, the private workspace for that call.
A frame holds:
- The method’s local variables.
- The parameter values passed in.
- Bookkeeping about where to return.
How a frame behaves:
- Pushed on top the moment the method is called.
- Popped off the top the moment it returns, which frees its memory automatically. You never clean up by hand.
Run the program below and watch the order of the messages.
public class Main { static void first() { System.out.println("first starts"); second(); System.out.println("first ends"); }
static void second() { System.out.println("second starts"); third(); System.out.println("second ends"); }
static void third() { System.out.println("third runs"); }
public static void main(String[] args) { first(); }}Trace the stack:
maincallsfirst→ frame forfirstpushed.firstcallssecond→ frame forsecondpushed.secondcallsthird→ frame forthirdpushed.thirdreturns → its frame popped.secondfinishes, prints “second ends”, returns → popped.firstfinishes, prints “first ends”, returns → popped.
Output
first startssecond startsthird runssecond endsfirst endsThe “ends” lines come in reverse order from the “starts” lines. That reverse order is the LIFO rule showing itself.
📦 What sits inside a frame
A frame holds the locals and parameters for one call, and each call gets its own fresh copy. The code below calls the same method twice.
public class Main { static void greet(String name) { String message = "Hello, " + name; // local in greet's frame System.out.println(message); }
public static void main(String[] args) { greet("Alex"); greet("Sam"); }}Walk through the frames:
greet("Alex")→ frame pushed,nameis “Alex”. Method returns, frame popped, locals gone.greet("Sam")→ a brand new frame,nameis “Sam”. It shares nothing with the first call.
Output
Hello, AlexHello, SamThis is why a local variable disappears the instant its method returns: it lived in a frame, and the pop throws it away.
🔗 Primitives and references on the stack
A primitive type is a plain built-in type like a number or a true/false flag. The two kinds of local store differently:
- A primitive (
int,boolean) stores its actual value right in the frame. - A variable that names an object stores a reference, a small value pointing to the object. The reference sits in the frame; the object sits on the heap.
The program below makes both kinds so you can see the split.
public class Main { public static void main(String[] args) { int count = 5; // value 5 sits in the frame int[] nums = {10, 20, 30}; // reference sits in the frame // the array object sits on the heap System.out.println(count); System.out.println(nums[0]); }}Where each piece lives:
countis a primitiveint. The number 5 sits directly insidemain’s frame.numsis a reference. The frame stores a small pointer; the array object lives on the heap.
Output
510So the rule: primitives and references live on the stack, but the objects those references point to live on the heap. The frame stays small on purpose.
🧵 Each thread gets its own stack
The stack is per-thread. Every thread gets its own stack, so threads never share frames or trip over each other’s locals. The code below starts a second thread.
public class Main { static void work(String who) { int step = 1; // each thread's frame has its own step System.out.println(who + " step " + step); }
public static void main(String[] args) { Thread t = new Thread(() -> work("worker")); t.start(); work("main"); }}Two stacks at work:
- The main thread pushes
work("main")on its own stack. - Thread
tpusheswork("worker")on its own separate stack. stepexists once per frame, so each thread has its own independentstep. They never collide.
The order of the two lines can vary, since both threads run at once. Here is one possible run.
Output
worker step 1main step 1This is why the heap is shared but the stack is not: heap objects can be reached by any thread holding a reference, while each thread’s calls and locals stay private on its own stack.
💥 Recursion grows the stack
Recursion is when a method calls itself, and each call pushes another frame. A safe recursion needs a base case, a condition where it stops calling itself and returns. The version below counts down to zero and stops.
public class Main { static void countDown(int n) { if (n <= 0) { // base case: stop calling itself System.out.println("done"); return; } System.out.println(n); countDown(n - 1); // each call pushes one more frame }
public static void main(String[] args) { countDown(3); }}Trace the frames piling up and unwinding:
countDown(3)prints 3, callscountDown(2). Two frames.countDown(2)prints 2, callscountDown(1). Three frames.countDown(1)prints 1, callscountDown(0). Four frames.countDown(0)hits the base case, prints “done”, returns. Frames pop off one by one back tomain.
Output
321doneA healthy recursion grows the stack, reaches its base case, then shrinks back down.
⚠️ When the stack overflows
With no base case, recursion keeps pushing frames forever. The stack has a limit, so once the frames fill it the JVM throws a StackOverflowError. The method below has no base case.
public class Main { // ❌ no base case: every call pushes a frame, none ever returns static void forever() { forever(); }
public static void main(String[] args) { forever(); }}Why it fails:
- Every call to
foreverpushes a frame, and none ever returns. - The frames stack until there is no room left, then the JVM gives up.
- The trace repeats the same line because the same method called itself over and over.
Output
Exception in thread "main" java.lang.StackOverflowError at Main.forever(Main.java:4) at Main.forever(Main.java:4) at Main.forever(Main.java:4) at Main.forever(Main.java:4) ...The fix is always the same: give the recursion a reachable base case so the frames can return and pop off.
Recursion always needs a base case
A method that calls itself with no reachable base case keeps adding stack frames until the stack is full, which gives a StackOverflowError. This is the most common way people meet that error. It has nothing to do with the heap or the garbage collector, because these are stack frames, not heap objects.
⚖️ Stack vs heap at a glance
This quick contrast keeps the two apart.
| Feature | Stack | Heap |
|---|---|---|
| Stores | Method frames, local variables, references | Objects created with new |
| Lifetime | Lasts only while the method runs | Lasts while something still refers to it |
| Shared between threads? | No, one stack per thread | Yes, shared by all threads |
| Speed | Very fast, simple push and pop | Slower, needs garbage collection |
| Cleanup | Automatic when the method ends | By the garbage collector |
| Error when full | StackOverflowError | OutOfMemoryError |
Short version: quick, ordered, per-method data goes on the stack; longer-lived, shared data goes on the heap.
⚠️ Common Mistakes
A few stack misunderstandings worth clearing up.
Thinking local variables stay around after the method ends. They live in a frame, and the frame is popped on return.
// ❌ wrong: "x is still in memory after the method returns"static void demo() { int x = 5; // x exists only while demo runs}// ✅ right: x is gone the moment demo's frame is poppedForgetting the base case in recursion. With no way to stop, the stack fills up.
// ❌ wrong: no base case, fills the stackstatic void loop() { loop();}// ✅ right: a base case lets the frames return and pop offstatic void loop(int n) { if (n <= 0) return; loop(n - 1);}Confusing the two errors. Deep recursion fills the stack, not the heap.
❌ wrong: "too much recursion causes OutOfMemoryError"✅ right: too much recursion causes StackOverflowError (stack)✅ right: too many live objects cause OutOfMemoryError (heap)✅ Best Practices
Habits that keep the stack working smoothly.
- Always give recursion a reachable base case. It is the simplest way to avoid a stack overflow.
- For very deep repetition, prefer a plain loop over recursion. A loop reuses one frame instead of pushing thousands.
- Keep methods focused. Short methods with few locals make small, fast frames.
- Remember the split. Primitives and references sit on the stack. Objects sit on the heap.
- Read the error name first.
StackOverflowErrorpoints you at the stack and almost always at recursion.
🧩 What You’ve Learned
Nicely done. Let’s recap Java stack memory.
- ✅ The call stack holds one frame for each method call. Each frame stores that method’s local variables and parameters.
- ✅ Frames are pushed on call and popped on return, following the LIFO rule. Cleanup is automatic.
- ✅ The stack is per-thread. Each thread has its own stack, so locals never collide between threads.
- ✅ Primitives and references live on the stack, but the objects those references point to live on the heap.
- ✅ Recursion grows the stack. Deep or infinite recursion with no base case causes a
StackOverflowError.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does the JVM push onto the stack for each method call?
Why: Each method call gets its own frame holding that call's locals and parameters.
- 2
In what order do stack frames come off the stack?
Why: The stack is LIFO: the most recently called method returns and pops first.
- 3
Where does a local int variable store its value?
Why: A primitive local stores its value right in the frame on the stack.
- 4
What usually causes a StackOverflowError?
Why: Each recursive call pushes a frame; with no base case the frames fill the stack.
🚀 What’s Next?
The stack cleans itself up automatically when methods return. The heap is different: objects there stick around until nothing refers to them, and then the garbage collector frees that space. Let’s see how it works next.