Java Stack vs. Heap Memory
How the Java stack and heap differ, what lives in each, and the lifecycle of variables and objects.
Java Stack vs. Heap Memory
At runtime the JVM splits the memory it manages into two regions with very different jobs. The stack holds the bookkeeping for method calls — one frame per call, with the method's local variables and primitive values inside it. The heap holds every object you create with new, shared by the whole program and reclaimed by the garbage collector. Almost every confusing Java behavior — why a method "can't change" your int, why two variables "see" the same edit, why deep recursion crashes — comes straight from this split.
Two regions, two lifecycles
The stack is per-thread and automatic: when a method is called the JVM pushes a frame, and when the method returns that frame is popped and its locals vanish instantly. The heap is shared and managed: objects live until no reference points to them, at which point the garbage collector is free to reclaim the space. Nothing on the heap disappears the moment a method returns.
| Aspect | Stack | Heap |
|---|---|---|
| Holds | Frames: locals, primitives, references | Objects, arrays, instance fields |
| Scope | One per thread | One shared by the whole JVM |
| Lifetime | Frame popped on method return | Until unreachable, then GC |
| Allocation | Push/pop, extremely fast | new, managed by the allocator |
| Sizing | Bounded (-Xss); overflow throws StackOverflowError | Bounded (-Xmx); exhaustion throws OutOfMemoryError |
| Cleanup | Automatic, deterministic | Garbage collector, non-deterministic |
What actually sits where
A local variable always lives in the current stack frame. What it holds depends on its type. For a primitive, the frame holds the value itself. For an object type, the frame holds only a reference — the object it points to lives on the heap.
void example() {
int count = 5; // the value 5 sits in the frame (stack)
double rate = 0.5; // likewise on the stack
int[] data = new int[3]; // 'data' (a reference) is on the stack,
// the 3-element array is on the heap
Point p = new Point(1, 2); // 'p' is on the stack, the Point is on the heap
} // frame popped: count, rate, data, p all gone;
// the array and Point survive until GCInstance fields are part of the object, so they live on the heap with it — even a field of primitive type. A private int balance inside an Account object is heap memory, not stack memory, because it belongs to the object, not to any one method call.
Java is pass-by-value — always
Java copies the argument into the parameter on every call. For a primitive, it copies the value; for an object, it copies the reference. There is no pass-by-reference in Java, and that single rule explains the three classic surprises:
static void bumpPrimitive(int n) { n++; } // changes the copy only
static void mutate(StringBuilder sb) { sb.append("!"); } // edits shared object
static void rebind(StringBuilder sb) { // points the copy elsewhere
sb = new StringBuilder("new"); // caller's variable unchanged
}bumpPrimitive cannot affect the caller: it received a copy of the number. mutate can change what the caller sees, because the copied reference still points at the caller's object on the heap. rebind cannot, because reassigning the parameter only rewires the local copy of the reference, not the caller's variable.
The string pool, a heap special case
String literals are interned: identical literals share one object in a pool, so == (reference identity) returns true for them. Writing new String("hi") forces a separate heap object, so == returns false even though the characters match. This is why you compare strings with .equals(), which checks contents, not identity.
String a = "hi";
String b = "hi";
String c = new String("hi");
a == b; // true — both point at the pooled literal
a == c; // false — c is a distinct heap object
a.equals(c); // true — same charactersWhen the regions run out
Each region has a limit and its own failure mode. Unbounded recursion keeps pushing frames until the stack is exhausted, raising StackOverflowError. Allocating objects faster than the GC can reclaim them exhausts the heap, raising OutOfMemoryError. Both are Errors, not Exceptions — signs of a structural problem (a missing base case, a leak) rather than something to routinely catch.
static int countDown(int n) {
return countDown(n - 1); // no base case -> StackOverflowError
}A worked example: watch the two regions behave
This program touches every rule above in one run: a primitive passed by value, two references to one heap object, a mutation that the caller sees, a reassignment it does not, the string pool, a deliberate stack overflow, and dropping the last reference to an object.
What to take from the run:
scorestays10while the method'snreaches110. The primitive was copied into the new frame, so nothing the method did could reach back intomain— this is pass-by-value for primitives, made visible.a.valueandb.valueare both42anda == bistrue, becauseCounter b = acopied a reference, not the object. One heap instance, two stack variables pointing at it — edit through either and both "see" it.- After
mutateThroughReference(a),a.valueis999. The method received a copy of the reference, but the copy still aimed at the same heap object, so the field change is visible to the caller. - After
reassignReference(a),a.valueis still999, not-1. Reassigning the parameter only rewired the method's local copy of the reference; the caller'sanever moved. Mutating the object works; rebinding the variable does not. lit1 == lit2istruebutlit1 == objisfalse, while.equalsistruefor both. Pooled literals share one heap object;new Stringforces a distinct one. The reached-but-survivableStackOverflowErrorand the finaltemp = nullshow the two regions' limits and how the heap becomes collectable when its last reference drops.
Practice
A method receives a parameter of type StringBuilder. Inside the method you call sb.append('x'). After the method returns, the caller's StringBuilder shows the appended 'x'. Why?