Java Stack Memory

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:

  • main calls first → frame for first pushed.
  • first calls second → frame for second pushed.
  • second calls third → frame for third pushed.
  • third returns → its frame popped.
  • second finishes, prints “second ends”, returns → popped.
  • first finishes, prints “first ends”, returns → popped.

Output

first starts
second starts
third runs
second ends
first ends

The “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, name is “Alex”. Method returns, frame popped, locals gone.
  • greet("Sam") → a brand new frame, name is “Sam”. It shares nothing with the first call.

Output

Hello, Alex
Hello, Sam

This 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:

  • count is a primitive int. The number 5 sits directly inside main’s frame.
  • nums is a reference. The frame stores a small pointer; the array object lives on the heap.

Output

5
10

So 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 t pushes work("worker") on its own separate stack.
  • step exists once per frame, so each thread has its own independent step. 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 1
main step 1

This 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, calls countDown(2). Two frames.
  • countDown(2) prints 2, calls countDown(1). Three frames.
  • countDown(1) prints 1, calls countDown(0). Four frames.
  • countDown(0) hits the base case, prints “done”, returns. Frames pop off one by one back to main.

Output

3
2
1
done

A 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 forever pushes 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 popped

Forgetting the base case in recursion. With no way to stop, the stack fills up.

// ❌ wrong: no base case, fills the stack
static void loop() {
loop();
}
// ✅ right: a base case lets the frames return and pop off
static 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. StackOverflowError points 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. 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. 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. 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. 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.

Java Garbage Collection

Share & Connect