Stack vs Heap Memory: What Every Developer Must Know
- Stack holds method frames (local variables, parameters, return addresses) and is automatically reclaimed when a method returns. Heap holds objects and is managed by the GC.
- Stack memory is thread-private β each thread has its own stack. Heap memory is shared across all threads, which is why heap objects need synchronisation.
- StackOverflowError = stack full from unbounded recursion. OutOfMemoryError = heap full from too many live objects.
Stack vs heap is one of those fundamentals that separates developers who write code from developers who understand what their code does at runtime. I've diagnosed StackOverflowErrors from unbounded recursion in production and tuned JVM heap flags for services handling 50,000 requests per minute. Both incidents would have been faster to resolve if the engineers involved had a clear mental model of memory allocation.
How Stack Memory Works
Every thread has its own call stack. When a method is called, the JVM pushes a new stack frame containing: the method's local variables and parameters, the method's return address, and intermediate computation results. When the method returns, the frame is popped and the memory is instantly reclaimed β no garbage collection, no overhead.
Stack allocation is O(1) β incrementing a pointer. That's why local primitives are fast. The downside: the stack is small (256KBβ1MB per thread by default). Deep or unbounded recursion fills it and throws StackOverflowError.
package io.thecodeforge.memory; public class StackMemoryDemo { public static void main(String[] args) { // 'multiplier' lives on main()'s stack frame int multiplier = 3; // A new stack frame is created for calculate(5) int result = calculate(5); System.out.println(result * multiplier); // 45 } // main() stack frame popped β 'multiplier' and 'result' gone static int calculate(int input) { int doubled = input * 2; // on calculate()'s stack frame int added = doubled + 25; // also on this frame return added; } // Frame popped β 'doubled' and 'added' are gone immediately // StackOverflowError: no base case, frames accumulate indefinitely static void unbounded(int n) { unbounded(n + 1); } // Safe: bounded recursion with a base case static int factorial(int n) { if (n <= 1) return 1; // Base case β stops recursion return n * factorial(n - 1); // At most n frames on stack } }
// unbounded() produces:
// Exception in thread 'main' java.lang.StackOverflowError
// at io.thecodeforge.memory.StackMemoryDemo.unbounded(StackMemoryDemo.java:16)
// at io.thecodeforge.memory.StackMemoryDemo.unbounded(StackMemoryDemo.java:16)
// ... (thousands more identical frames)
How Heap Memory Works
The heap is where objects live. new PaymentService() allocates the object on the heap. The reference (a 4-8 byte pointer) to that object lives on the stack (or inside another object on the heap). When no references point to an object, it becomes eligible for garbage collection.
The JVM heap is divided into generations. Young generation holds newly created objects β minor GC runs frequently here and is fast. Objects that survive several minor GCs are promoted to Old generation β collected less often, in a major GC that can pause the application. Creating millions of short-lived objects in a hot path causes 'GC pressure' β frequent minor GCs that add latency spikes even when total memory use seems fine.
Configure heap size with JVM flags: -Xms for initial size, -Xmx for maximum. Each thread's stack size is configured with -Xss.
package io.thecodeforge.memory; public class HeapMemoryDemo { // Object fields live on the heap (part of the object) static class PaymentRecord { String paymentId; // reference on heap; String object also on heap int amountPence; // primitive stored directly in the object on heap boolean processed; } public static void main(String[] args) { // 'record' reference: on main()'s stack frame (4-8 bytes) // PaymentRecord object: on the heap (size = fields + object header ~16 bytes) PaymentRecord record = new PaymentRecord(); record.paymentId = "pay-42"; record.amountPence = 10_000; // Β£100.00 record.processed = false; process(record); // passes the reference β NOT a copy of the object record = null; // No more references β object eligible for GC } // 'record' reference popped from stack; object waits for GC static void process(PaymentRecord r) { // 'r' is a copy of the reference on THIS frame's stack // r.processed = true modifies the SAME object on the heap r.processed = true; System.out.println("Processed: " + r.paymentId); } // 'r' reference gone; heap object still exists (main() still holds it) // JVM flags for memory tuning: // java -Xms512m -Xmx2g -Xss512k -jar app.jar // ^^^^^^^^ ^^^^ ^^^^^^^ // init heap max stack per thread }
// Memory layout while process() runs:
// Stack (main frame): record β 0x1a2b3c4d
// Stack (process frame): r β 0x1a2b3c4d (same address)
// Heap (0x1a2b3c4d): PaymentRecord{paymentId='pay-42', amountPence=10000, processed=true}
| Characteristic | Stack | Heap |
|---|---|---|
| Stores | Local vars, params, references | Objects, instance fields, arrays |
| Managed by | JVM (LIFO, automatic) | Garbage Collector |
| Size | 256KBβ1MB per thread (default) | Configurable with -Xmx (GBs) |
| Speed | Very fast (pointer increment) | Slower (allocation + GC overhead) |
| Lifetime | Method execution scope | Until no references remain |
| Error when full | StackOverflowError | OutOfMemoryError |
| Thread access | Private per thread | Shared by all threads |
| GC involved? | No | Yes |
π― Key Takeaways
- Stack holds method frames (local variables, parameters, return addresses) and is automatically reclaimed when a method returns. Heap holds objects and is managed by the GC.
- Stack memory is thread-private β each thread has its own stack. Heap memory is shared across all threads, which is why heap objects need synchronisation.
- StackOverflowError = stack full from unbounded recursion. OutOfMemoryError = heap full from too many live objects.
- Passing an object to a method passes the reference (cheap pointer copy). The method sees the same heap object and can mutate its fields.
- JVM flags: -Xms (initial heap), -Xmx (max heap), -Xss (stack size per thread). Tune these based on your application's object allocation profile.
β Common Mistakes to Avoid
- βAssuming primitives always live on the stack β primitive fields of an object live on the heap as part of that object. Only local primitive variables in method bodies live on the stack.
- βConfusing passing a reference with copying an object β when you pass an object to a method, you pass the reference (4-8 bytes). The method can modify the object's fields but cannot change what the caller's variable points to.
- βNot understanding that stack is thread-private β each thread has its own stack. Race conditions can only occur on heap objects accessed by multiple threads, not on local stack variables.
- βCreating large temporary objects in hot loops β each allocation goes to Young generation. Frequent minor GC cycles from unnecessary allocations cause latency spikes even when memory seems plentiful.
Interview Questions on This Topic
- QWhat is the difference between stack and heap memory? Where does a Java object live?
- QA method creates a List<String> with 10,000 elements and passes it to another method. What happens in memory?
- QWhy is stack memory thread-safe while heap memory is not?
- QWhat causes StackOverflowError vs OutOfMemoryError?
Frequently Asked Questions
What is the difference between stack and heap memory in Java?
The stack stores method frames containing local variables, parameters, and return addresses. It's thread-private and automatically reclaimed when a method returns. The heap stores objects created with 'new' and is managed by the garbage collector. Objects live on the heap until no references point to them.
Where do primitive variables live in Java?
It depends on context. Primitive local variables and method parameters live on the stack. Primitive fields of an object live on the heap as part of that object's memory block.
What causes StackOverflowError?
Unbounded recursion β a method that calls itself without a base case that terminates. Each call adds a frame to the stack. When the stack exhausts its allocated space (typically 256KBβ1MB per thread), the JVM throws StackOverflowError.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.