Java Tutorial
🔍

Java Stack vs Heap Memory

Java Stack vs Heap Memory

Stack and heap are the two memory regions every Java method touches on every single call, and confusing what lives where is one of the fastest ways to misunderstand how Java actually passes data into a method. A primitive local variable and an object reference can sit right next to each other on the same stack frame, yet behave completely differently the moment either one is passed into another method — and that difference is entirely explained by where the actual data lives.

What Is the Difference Between Stack and Heap Memory?

AspectStack MemoryHeap Memory
What it storesLocal variables, method parameters, object references, return addressesActual objects and arrays created with new
Allocated perEach thread gets its own private stackShared across all threads in the JVM
LifetimeFrame created on method call, destroyed the moment the method returnsObject lives as long as something reachable still references it
Access speedVery fast, a simple push and popSlower, involves the garbage collector's bookkeeping
SizeFixed per thread, configurable with -XssOne shared JVM-wide limit, configurable with -Xms and -Xmx
Failure modeStackOverflowError from excessive recursionOutOfMemoryError: Java heap space when full
Managed byAutomatic, tied directly to method call and returnThe garbage collector

Every one of those rows traces back to one root idea: the stack tracks where a thread currently is in its own chain of method calls, while the heap holds the data those calls actually work with.

How They Work Together During a Method Call

One sentence before the diagram: a primitive local variable stores its actual value directly on the stack, while an object reference stores only a pointer, with the real object sitting separately on the heap.

Stack (per thread)                      Heap (shared)

processOrder() frame
  discountPercent = 10        (primitive value, stored directly)
  order  ------------------------->  Order object
                                        { itemCount: 3,
                                          total: 1500.0 }

discountPercent never needs to look anywhere else — its value is right there on the stack. order is different: the stack only holds a reference, an address pointing at the actual Order object living on the heap. Both values disappear the moment processOrder() returns, but only discountPercent's actual data goes with it — the Order object stays on the heap for as long as something else still references it.

Code Examples

Passing a primitive into a method hands the method a private copy — nothing the method does to that copy can reach back to the caller.

1// File: PricingService.java 2 3public class PricingService { 4 5 static class Order { 6 int itemCount; 7 double total; 8 9 Order(int itemCount, double total) { 10 this.itemCount = itemCount; 11 this.total = total; 12 } 13 } 14 15 static void applyDiscount(double discountPercent) { 16 discountPercent = discountPercent + 100; 17 // This reassignment only changes the local copy on this method's 18 // own stack frame - the caller's variable is completely unaffected 19 } 20 21 static void markAsPacked(Order order) { 22 order.itemCount = 0; 23 // order is a reference stored on this method's stack frame, but it 24 // points to the same heap object the caller is holding - mutating 25 // a field through it is visible to the caller too 26 } 27 28 public static void main(String[] args) { 29 double callerDiscount = 10.0; 30 applyDiscount(callerDiscount); 31 System.out.println("Discount after method call: " + callerDiscount); 32 33 Order order = new Order(3, 1500.0); 34 markAsPacked(order); 35 System.out.println("Item count after method call: " + order.itemCount); 36 } 37}
Output:
Discount after method call: 10.0
Item count after method call: 0

callerDiscount never changes, because applyDiscount only ever touched its own private copy. order.itemCount does change, because markAsPacked received a copy of the reference, not the object — and that copy still points at the exact same heap object main is holding.

Real-World Example

A checkout service calculates a discounted price, and the way the result gets back to the caller is where this whole distinction turns into a genuine, easy-to-make bug.

1// File: DiscountCalculator.java 2 3public class DiscountCalculator { 4 5 static double applyDiscountWrong(double price, double discountPercent) { 6 price = price - (price * discountPercent / 100); 7 return price; 8 } 9 10 static double applyDiscountCorrect(double price, double discountPercent) { 11 return price - (price * discountPercent / 100); 12 } 13 14 public static void main(String[] args) { 15 double originalPrice = 2000.0; 16 17 applyDiscountWrong(originalPrice, 10.0); 18 System.out.println("Price after calling without using the return value: " + originalPrice); 19 20 double discountedPrice = applyDiscountCorrect(originalPrice, 10.0); 21 System.out.println("Price after using the returned value: " + discountedPrice); 22 } 23}
Output:
Price after calling without using the return value: 2000.0
Price after using the returned value: 1800.0

applyDiscountWrong genuinely computes the discounted price correctly inside itself, but the first call in main throws that result away — originalPrice was never going to change just because a method that happened to receive a copy of it did some math internally. A mistake that appears often in fresher pull requests is calling a calculation method like this and assuming the caller's variable updates automatically, since the method's own body looks like it is updating price. Java never passes primitives by reference — the only way a method can hand a new value back is by returning it, and the caller has to actually use that returned value. During code reviews, seniors commonly flag a discarded return value from a calculation method as exactly this kind of bug waiting to surface, especially once the code gets refactored and someone assumes the original call was doing something it never actually did.

Best Practices

Keep primitive parameters read-only in your own head when reading someone else's method — never assume a method can mutate a caller's primitive.

When a method needs to hand back a new primitive value, always return it, and check that every call site actually uses that return value.

For objects, be deliberate about mutation — if a method is not meant to change the caller's object, do not mutate its fields; return a new object instead, which is exactly why immutable design, covered in this series' dedicated Immutable Class article, avoids this entire class of bug.

Never describe Java as "pass by reference for objects" — Java is always pass by value, and for an object, the value being copied is the reference itself, not the object.

Common Mistakes

Assuming Java passes objects by reference the way some other languages do is the root of most confusion here. Java always passes by value — for an object, the value being copied is a reference, which behaves differently from copying the object itself in one specific way worth seeing directly.

1// File: ReferenceReassignmentMistake.java 2 3public class ReferenceReassignmentMistake { 4 5 static class Order { 6 int itemCount; 7 Order(int itemCount) { this.itemCount = itemCount; } 8 } 9 10 static void replaceOrder(Order order) { 11 order = new Order(99); 12 // This reassigns the local copy of the reference to point at a 13 // brand new object - the caller's variable still points at the 14 // original object, completely unaffected by this line 15 } 16 17 public static void main(String[] args) { 18 Order original = new Order(3); 19 replaceOrder(original); 20 21 System.out.println("Item count: " + original.itemCount); 22 } 23}
Output:
Item count: 3

replaceOrder reassigns its own local copy of the reference to point at a brand new Order, but original back in main was never touched — it still points at the first object it always did. Reassigning a reference parameter and mutating a field through it are two entirely different operations, and only one of them is visible to the caller.

Interview Questions

Q1. Is Java pass by value or pass by reference?

Java is always pass by value — there is no pass-by-reference in Java at all. For primitives, the value copied is the primitive itself; for objects, the value copied is the reference, never the object. Interviewers specifically want to hear "always pass by value," since "sometimes pass by reference for objects" is the single most common wrong answer to this exact question.

Q2. If Java is pass by value, why does modifying an object inside a method affect the caller?

Because the copied reference still points to the same heap object the caller's reference points to. Mutating a field through that copied reference changes the one shared object both the caller and the method are looking at — nothing about the reference itself needed to be shared, only the object it points to. This is the direct follow-up interviewers use to check you understand why the previous answer is true, not just that you memorized it.

Q3. What is the difference between reassigning an object reference inside a method and mutating a field on that object?

Mutating a field through the reference changes the shared heap object, so the caller sees it. Reassigning the local reference variable to point somewhere else only changes what that one local variable points to — the caller's own variable still points at the original object, exactly as demonstrated in this article's Common Mistakes section. This exact distinction is what separates a strong answer from a shaky one on this whole topic.

Q4. Why does each thread get its own stack, but all threads share one heap?

Each thread executes its own independent sequence of method calls, so each needs its own private stack to track its own call chain, local variables, and return addresses without interfering with any other thread's calls. The heap holds actual objects that are frequently meant to be shared across multiple threads at once, so one shared heap makes that possible. Interviewers listen for the phrase "independent call chain" as evidence you understand why per-thread stacks exist, not just that they do.

Q5. What is the difference between a StackOverflowError and an OutOfMemoryError: Java heap space?

StackOverflowError happens when a thread's stack runs out of space, almost always from runaway or excessively deep recursion. OutOfMemoryError: Java heap space happens when the heap is full of reachable objects and garbage collection cannot free enough room for a new allocation. Interviewers are testing whether you connect each error to the specific memory region that actually ran out, rather than treating both as one generic "out of memory" error.

Q6. Where do local variables live compared to the objects they reference?

A local variable — whether a primitive or an object reference — always lives on the stack frame of the method it was declared in. The actual object data that reference points to lives on the heap, potentially long after the method that created it has returned, provided something still holds a reference to it. Watch for candidates who say "the object lives on the stack" — that is the single most common wrong answer here.

Q7. Can a String be mutated the way the Order example's field was mutated in this article?

No — String is immutable in Java, so none of its methods ever mutate the underlying character data. Every operation that looks like a modification, such as concatenation, actually creates and returns a brand new String object on the heap, leaving the original untouched. Interviewers use this to check whether you understand immutability as a deliberate design choice, and precisely why it makes String safe to share freely without the kind of mutation risk this article's Order example demonstrates.

FAQs

Does every local variable live on the stack?

Yes — every local variable, whether it holds a primitive value or an object reference, is stored on the stack frame of the method where it was declared. Only the object data an object reference points to lives separately on the heap.

Why do people say Java is pass by reference for objects if it technically is not?

Because modifying an object's field through a method parameter is visible to the caller, which looks like pass-by-reference behavior from the outside. Technically, it is the reference's value being copied by value, not the object itself, which is exactly why reassigning that copied reference inside the method never affects the caller.

Can I increase the size of the stack if I keep getting StackOverflowError?

Yes, using the -Xss JVM flag, though a StackOverflowError from genuinely runaway or unintentional recursion is almost always better fixed by correcting the recursion or converting it to an iterative approach, rather than just raising the limit.

Is accessing stack memory faster than accessing heap memory?

Yes, generally — stack access is a simple, predictable push and pop with no bookkeeping involved, while heap access can involve the garbage collector's tracking and, depending on the collector, occasional pauses.

What happens to stack memory when a method finishes running?

Its entire stack frame — local variables, parameters, everything specific to that call — is popped off the stack and reclaimed immediately when the method returns, with no garbage collector involvement needed at all.

Do arrays live on the stack or the heap?

Arrays are objects in Java, so the array itself lives on the heap, exactly like any other object. Only the variable referencing that array lives on the stack.

Is understanding stack vs heap actually useful day to day, or just for interviews?

Both — beyond interviews, it directly explains why passing an object into a method and mutating it affects the caller while passing a primitive never does, exactly the kind of bug this article's Common Mistakes section walks through and one that shows up in real pull requests regularly.

Summary

Stack and heap are not really two competing ideas to memorize separately — they are two halves of the same story about where data lives during a single method call. A primitive on the stack is always a private copy; an object reference on the stack always points at something shared on the heap, and everything about pass-by-value behavior in Java falls directly out of that one distinction.

The reassignment-versus-mutation contrast in this article's examples is worth carrying into every code review from here on, since it is exactly the kind of detail that separates confident, correct reasoning about a method's side effects from a guess.

What to Read Next