Stack Memory
Stack memory is the region of memory where the JVM stores method invocation frames, local variables, and partial results. Every thread has its own private stack created at thread creation, and the stack grows and shrinks as methods are called and return. Stack memory operates on a last-in, first-out discipline — the frame for the most recently called method sits on top, and when that method returns its frame is immediately discarded. Understanding stack memory explains why local variables are thread-safe by default, why recursive algorithms can cause StackOverflowError, why primitive values behave differently from objects, and what the JVM does at every method call and return. This entry covers stack frame structure, the stack pointer, local variable storage, operand stacks, frame lifecycle, thread isolation, and the performance characteristics that make stack allocation extremely fast.
What the Stack Is and How It Works
// ── Stack frame creation and destruction ──────────────────────────────
//
// Call: main() → methodA() → methodB() → methodC()
//
// Stack grows UPWARD as methods are called:
//
// ┌───────────────┐ ← top of stack (most recent frame)
// │ methodC() │ frame 4 — currently executing
// │ local: z=3 │
// ├───────────────┤
// │ methodB() │ frame 3 — waiting for methodC to return
// │ local: y=2 │
// ├───────────────┤
// │ methodA() │ frame 2 — waiting for methodB to return
// │ local: x=1 │
// ├───────────────┤
// │ main() │ frame 1 — bottom of current thread's stack
// │ local: args │
// └───────────────┘ ← bottom of stack (thread entry point)
//
// When methodC returns: frame 4 is IMMEDIATELY discarded (stack pointer decremented)
// When methodB returns: frame 3 is discarded
// etc.
// ── What lives on the stack vs what lives on the heap ─────────────────
public void exampleMethod() {
int x = 42; // primitive — VALUE stored directly on stack
double d = 3.14; // primitive — VALUE stored directly on stack
boolean flag = true; // primitive — VALUE stored directly on stack
String s = "Hello"; // REFERENCE (address) on stack, object on HEAP
List<Integer> list = new ArrayList<>(); // REFERENCE on stack, ArrayList on HEAP
// x, d, flag: entire value lives on this frame
// s: 8-byte reference address lives on this frame; String object is on heap
// list: 8-byte reference address lives on this frame; ArrayList is on heap
}
// ── Thread isolation — every thread has its OWN stack ─────────────────
// Thread 1 Thread 2
// ┌──────────────────┐ ┌──────────────────┐
// │ Stack (private) │ │ Stack (private) │
// │ method() x=5 │ │ method() x=99 │
// └──────────────────┘ └──────────────────┘
//
// Thread 1's x=5 and Thread 2's x=99 are in completely separate memory
// No race condition possible — no synchronisation needed for local varsStack Frame Structure
// ── What bytecode looks like for a simple method ─────────────────────
public int add(int a, int b) {
int result = a + b;
return result;
}
// Bytecode (from javap -c):
// 0: iload_1 // push 'a' from local slot 1 onto operand stack
// 1: iload_2 // push 'b' from local slot 2 onto operand stack
// 2: iadd // pop a and b, push their sum
// 3: istore_3 // pop sum, store in local slot 3 ('result')
// 4: iload_3 // push 'result' from slot 3 onto operand stack
// 5: ireturn // pop and return the top value to caller
// Local variable array for add(int a, int b):
// Slot 0: 'this' (implicit for instance methods)
// Slot 1: 'a' (first parameter)
// Slot 2: 'b' (second parameter)
// Slot 3: 'result' (local variable)
// ── long and double occupy TWO slots ──────────────────────────────────
public void longExample(long bigNum, double rate) {
// Slot 0: 'this'
// Slot 1-2: 'bigNum' (long — 64-bit, two slots)
// Slot 3-4: 'rate' (double — 64-bit, two slots)
long doubled = bigNum * 2;
// Slot 5-6: 'doubled'
}
// ── Slot reuse — compiler reuses slots for non-overlapping variables ──
public void slotReuse() {
{
int x = 10; // slot 1
} // x out of scope here
{
int y = 20; // compiler assigns slot 1 — x's slot is reused
}
}
// ── Instance vs static method frame ──────────────────────────────────
// Instance method: slot 0 = 'this', slot 1+ = parameters
// Static method: slot 0 = first parameter (no 'this')
public static int staticAdd(int a, int b) {
// Slot 0: 'a' (no 'this' for static)
// Slot 1: 'b'
return a + b;
}StackOverflowError — When the Stack Fills
// ── Infinite recursion — most common cause ────────────────────────────
public int factorial(int n) {
return n * factorial(n - 1); // no base case — infinite recursion!
// Each call pushes a new frame
// Eventually: java.lang.StackOverflowError
}
// Fixed: add base case
public int factorial_correct(int n) {
if (n <= 1) return 1; // base case — terminates recursion
return n * factorial_correct(n - 1);
}
// ── Mutual recursion ──────────────────────────────────────────────────
// isEven and isOdd call each other — equally dangerous
boolean isEven(int n) { return n == 0 || isOdd(n - 1); } // dangerous without limit
boolean isOdd(int n) { return n != 0 && isEven(n - 1); } // dangerous without limit
// ── Stack depth depends on frame size ────────────────────────────────
// Small frames → deeper recursion before overflow:
void smallFrame(int n) {
if (n == 0) return;
smallFrame(n - 1); // few local vars — small frame
}
// Large frames → shallower recursion before overflow:
void largeFrame(int n) {
long a, b, c, d, e, f, g, h; // many local vars — larger frame
double x, y, z, w;
if (n == 0) return;
largeFrame(n - 1);
}
// ── Converting recursion to iteration — eliminates overflow risk ───────
// Recursive DFS — overflow risk for deep trees:
void dfsRecursive(TreeNode node) {
if (node == null) return;
visit(node);
dfsRecursive(node.left);
dfsRecursive(node.right);
}
// Iterative DFS — uses heap (Deque), no overflow risk:
void dfsIterative(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<>();
if (root != null) stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
visit(node);
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
}
// ── JVM flag for stack size ───────────────────────────────────────────
// java -Xss2m MyApp → 2MB stack per thread
// java -Xss512k MyApp → 512KB stack per thread (default on many JVMs)
// Increasing -Xss costs: stackSize × threadCount memory
// 500 threads × 2MB stack = 1GB just for stacksPerformance Characteristics and Escape Analysis
// ── Escape analysis — object may not reach the heap ──────────────────
public int sumList(int[] data) {
int sum = 0;
// A naive implementation might box each int into Integer
// JIT escape analysis can eliminate the boxing entirely
for (int value : data) {
sum += value;
}
return sum;
}
// ── Object that does NOT escape — JIT may stack-allocate ─────────────
public int computeWithPoint() {
Point p = new Point(3, 4); // p does not escape this method
return (int) Math.sqrt(p.x * p.x + p.y * p.y);
// JIT may eliminate the Point allocation entirely:
// int px = 3, py = 4; return (int) Math.sqrt(px*px + py*py);
}
// ── Object that DOES escape — must be heap-allocated ──────────────────
private Point storedPoint;
public Point createAndStore() {
Point p = new Point(3, 4);
this.storedPoint = p; // p escapes: stored in heap field
return p; // p escapes: returned to caller
// Cannot be stack-allocated — JIT will heap-allocate it
}
// ── Diagnosing escape analysis with JVM flags ─────────────────────────
// -XX:+PrintEscapeAnalysis (product builds may need -XX:+UnlockDiagnosticVMOptions)
// -XX:+PrintEliminateAllocations shows which allocations were eliminated
// -XX:+DoEscapeAnalysis enable (default in HotSpot)
// -XX:-DoEscapeAnalysis disable (for comparison)
// ── Thread stack sizing guidance ──────────────────────────────────────
// Typical defaults (64-bit HotSpot):
// Client JVM: 320KB default
// Server JVM: 512KB default
// Platform threads: typically 512KB - 1MB
// Virtual threads (Java 21+): start very small (~1KB), grow lazily
//
// Virtual threads revolutionise this: they have tiny stacks that grow
// and shrink on demand, stored on the heap when not scheduled
// Thousands or millions of virtual threads become practical