Blog

Java Heap vs Stack

Key Takeaways

  • The Java heap holds objects and arrays in the JVM's abstract model. Each thread has a Java stack of method frames containing local variables and execution state.
  • A local reference and the object it points to are different things. Passing a reference to a method doesn't copy the object.
  • Garbage collection follows reachability, not application usefulness. Unreachable cycles can be collected; accidentally retained objects can cause memory leaks.
  • StackOverflowError usually means excessive call depth. OutOfMemoryError can involve the heap, Metaspace, or other resource limits—not just too many objects.

The difference in Java heap vs stack is primarily about objects versus method execution: the heap provides garbage-collected object storage, while each thread's stack tracks its active method calls. A reference in a method frame can point to a heap object whose lifetime extends beyond that method call. These are logical JVM concepts, not guarantees about every byte's physical location.

Preface: Java Memory Management

Java applications have finite memory, but the JVM doesn't divide all of it into just heap and stack. Class metadata, generated machine code, and native allocations also consume memory outside the Java object heap. Consequently, a healthy-looking heap doesn't prove that the entire process has enough memory.

JVM process: conceptual HotSpot memory areas
|
+-- Java heap                 Objects and arrays
+-- Thread A's Java stack     Method frames
+-- Thread B's Java stack     Method frames
+-- Metaspace                 Native class metadata
+-- Code cache                Generated machine code
+-- Other native allocations  JVM and native-library resources

Exact physical layout depends on the JVM and thread implementation.

Automatic memory management removes the need to explicitly free ordinary Java objects. It doesn't prevent excessive allocation, unintended retention, or excessive recursion. Understanding which resource is exhausted is more useful than treating every failure as a generic memory problem.

Heap vs Stack: A Practical Example

Save this program as MyClass.java:

public class MyClass {
    private static void printObject(Object param) {
        String str = param.toString();
        System.out.println(str);
    }

    public static void main(String[] args) {
        int i = 1;
        Object myObject = new Object();
        printObject(myObject);
    }
}

Compile and run it with a JDK:

javac MyClass.java
java MyClass

The program creates an object and prints its string representation. The unused i is intentional: it gives us a primitive local variable to compare with the reference locals.

  1. main has a frame containing its parameters and local variables, including the primitive value i and reference value myObject.
  2. new Object() creates an object, represented separately from the variable that refers to it.
  3. Calling printObject(myObject) gives param a copy of the reference value. Both references point to the same object.
  4. str is another local reference. The String returned by param.toString() is an object, not the local variable itself.

Java passes arguments by value, including reference values. Reassigning param wouldn't reassign myObject; modifying a mutable object through either reference can affect the same underlying object.

Conceptual snapshot inside printObject, after toString returns

Thread's Java stack                    Java heap
+-------------------------+
| printObject frame       |
|   param ----------------+----------> Object A
|   str ------------------+----------> String B
+-------------------------+                  ^
| main frame              |                  |
|   i = 1                 |          returned by A.toString()
|   myObject -------------+----------> Object A
|   args -----------------+----------> String[]
+-------------------------+

Both arrows to Object A identify the same object.
Other runtime objects and temporary values are omitted.

Primitive Fields Aren't Stack Locals

A variable's type alone doesn't determine where its value belongs. Compare the local i with these instance fields:

// Fragment: an example class with primitive and reference fields.
final class Item {
    int count = 1;
    Object payload = new Object();
}

For an allocated Item instance, count is part of the object, despite being primitive. payload is also a field in that object, holding a reference to a separate object. References aren't confined to method frames: fields and array elements can hold them too.

The JVM Model Isn't a Physical Allocation Guarantee

A just-in-time compiler, or JIT compiler, can optimize away source-level storage. Escape analysis determines whether an object's references escape a method or thread; scalar replacement can replace an object with its individual field values and eliminate its allocation.

Don't translate that into “all non-escaping objects move onto the stack.” HotSpot's documented server-compiler optimization eliminates suitable allocations rather than simply replacing each heap allocation with a stack allocation. Our Java JIT compiler explanation covers the broader compilation model.

Java Heap Space

Why Heap Space?

Objects often need to outlive the method that created them. A method can return an object, store it in another object, or make it available to another thread; tying that object's lifetime to the creating method's frame wouldn't work.

The heap supports these flexible lifetimes, while garbage collection manages reclamation. The trade-off is that allocation rate and retained live data influence collection work and memory pressure. A heap object may be reclaimed long before application shutdown; heap storage doesn't imply application-long lifetime.

Garbage Collection Follows Reachability

A GC root is a starting point from which the collector traces references. Roots include live references associated with executing threads and internal JVM references. Objects reachable through static fields of live classes can also remain alive.

For ordinary strong references, an object becomes eligible for reclamation when no path from a root reaches it. “Eligible” doesn't mean “freed immediately,” and “the application no longer needs it” doesn't mean “unreachable.” See our Java garbage collection guide for more detail.

This runnable example contrasts an isolated cycle with an object retained by a static collection:

import java.util.ArrayList;
import java.util.List;

public class ReachabilityExample {
    private static final List<Object> RETAINED = new ArrayList<>();

    private static final class Node {
        Node next;
    }

    private static void createCycle() {
        Node first = new Node();
        Node second = new Node();
        first.next = second;
        second.next = first;
    }

    private static void retainObject() {
        RETAINED.add(new Object());
    }

    public static void main(String[] args) {
        createCycle();
        retainObject();
        System.out.println("Example complete");
    }
}

Save it as ReachabilityExample.java, then compile and run it as you did MyClass. After createCycle returns, neither node is reachable from outside the cycle, so their references to each other don't prevent collection. The separate retained object remains reachable through RETAINED while the defining class remains live. Repeated, unbounded retention like this can become a memory leak in a long-running service. This program illustrates eligibility; it doesn't demonstrate when collection occurs.

Young and Old Generations Depend on the Collector

Generational collectors separate objects by age to concentrate some collection work on recently allocated objects. G1 uses young and old generations distributed across heap regions; its collection events include young-only and mixed collections, with mixed collections also reclaiming selected old regions.

G1 is the default on common HotSpot server configurations, but the actual selection depends on the JVM, platform, version, and options. Don't treat “minor” and “major” as universal event definitions: interpret GC logs using the documentation for your active collector.

Metaspace, Class Metadata, and Static Fields

HotSpot removed the Permanent Generation, or PermGen, in JDK 8. Class metadata now uses native memory managed as Metaspace rather than an old-style permanent generation in the Java heap. Metadata is deallocated when the corresponding classes are unloaded.

Class unloading isn't triggered simply because one instance disappears. An ordinary class can be unloaded only if its defining class loader can be reclaimed. By default, hidden classes defined through MethodHandles.Lookup.defineHiddenClass can unload while their defining loader remains reachable; the STRONG option gives them the ordinary loader-linked lifetime. Eligible unloading remains implementation-dependent.

A static field belongs to its class rather than to each instance. A static reference and the heap object it references are distinct from the JVM's native class metadata; don't lump all three together as “classes stored in the heap.”

Java Stack Memory

A method frame is the execution record for one method invocation. In the JVM model, it contains local variables, an operand stack for intermediate calculations, and information supporting linking and method completion. Frames are created on invocation and discarded when execution completes normally or unwinds because of an exception.

Frames follow last in, first out, or LIFO, ordering. In the example, printObject completes before control returns to main. Discarding its frame doesn't directly free the objects referenced by its locals; their reclamation depends on reachability.

Java Heap vs Stack: Practical Differences

ConcernJava heapJava stack
Primary roleObjects and arraysMethod frames and execution state
OwnershipShared JVM area; an individual object need not be sharedLogical stack private to each thread
LifetimeReclamation follows object reachabilityFrame lifetime follows its method invocation
Typical sizing controls in HotSpot-Xms and -Xmx-Xss for platform-thread stack sizing
Typical failureOutOfMemoryError: Java heap spaceStackOverflowError from excessive call depth

These distinctions describe responsibilities, not a speed ranking. Frame storage has a bounded invocation lifetime and generally doesn't require object-by-object garbage collection. Heap allocation, however, is highly optimized, and JIT compilation can remove allocations entirely. Measure your workload rather than assuming “stack is always faster.”

Does Heap Memory Need Synchronization?

Heap location doesn't make an object inherently safe or unsafe. A thread's local reference can point to a mutable object also referenced by another thread. Correct sharing requires an appropriate design, such as synchronization, concurrent collections, or safely published immutable state.

The private reference variable isn't the shared state; the referenced object's fields may be. Our Java multithreading examples put those access rules into practice.

Heap Capacity and JVM Memory Errors

Maximum, Committed, and Used Heap

The JVM manages heap capacity rather than necessarily allocating and using the entire maximum at startup. Distinguish these measurements:

  • Used: heap memory currently occupied, including garbage not yet reclaimed.
  • Committed: memory guaranteed to be available for the JVM to use; this amount can change over time.
  • Maximum: the upper heap capacity, not a guarantee that all of that capacity can be committed successfully.

Used memory is no greater than committed memory, which is no greater than a defined maximum. These numbers aren't interchangeable with total process memory.

In HotSpot, -Xms sets the initial and minimum heap size, and -Xmx sets the maximum heap size. -Xss controls platform-thread stack sizing; defaults and supported sizes depend on the platform. Increasing heap capacity doesn't increase a thread's stack capacity.

StackOverflowError vs OutOfMemoryError

  • StackOverflowError: a computation needs more stack than permitted, commonly because recursion is too deep. Inspect repeated frames and termination conditions before increasing stack size.
  • OutOfMemoryError: Java heap space: the JVM can't satisfy a heap allocation. Investigate retained objects, allocation volume, and capacity rather than assuming a leak.
  • OutOfMemoryError: Metaspace: class metadata storage is exhausted. Investigate class loading, class-loader retention, and metadata capacity.
  • Native-thread creation failure: can produce OutOfMemoryError because native memory or process/thread resource limits prevent another thread from starting. This isn't the same as one thread overflowing its stack.

Other native allocation failures can cause a fatal JVM error rather than a recoverable Java exception. Read the complete error message and any fatal-error log before changing memory settings.

Inspect a Running JVM with jcmd

With a JDK installed, use these diagnostic commands against a running application. Replace 12345 with its process ID; the short-lived examples above may already have exited.

# Confirm which diagnostic commands this JVM supports.
jcmd 12345 help

# Inspect current JVM flags and Java heap information.
jcmd 12345 VM.flags
jcmd 12345 GC.heap_info

VM.flags reports VM options and their current values; GC.heap_info reports heap information. Command availability and output vary by JVM. Run locally with the required effective user and group permissions; container boundaries and attach restrictions can affect access. Oracle documents GC.heap_info as a medium-impact operation, so don't assume diagnostic polling is free.

Common Mistakes

  • Confusing scope with reachability. Leaving a method removes its frame, not every object that method created.
  • Calling every OutOfMemoryError a heap leak. Identify the exhausted resource first.
  • Using memory location as a concurrency guarantee. Inspect which objects are shared and how their state is accessed.
  • Tuning before measuring. Check the actual JVM flags, collector, and memory usage before changing capacity.

Conclusion

Keep two questions separate: “Where is this variable represented?” and “What keeps this object alive?” Method frames explain execution and local variables; reachability explains object lifetime. That distinction is the useful foundation for understanding Java memory behavior—not blanket claims about speed or thread safety.

FAQ

What is the difference between heap and stack memory in Java?

The heap is the JVM's shared object-storage area, while each thread has a logical stack of method frames containing local variables and execution state. A local reference can point to a heap object that outlives the method.

Are Java objects always allocated on the heap?

The JVM specification models class instances and arrays as heap allocations. A JIT compiler can eliminate some physical allocations through optimizations such as scalar replacement, so source code doesn't guarantee a particular physical layout.

What happens when stack space is exhausted?

If a thread's computation requires more stack than permitted, the JVM throws StackOverflowError, commonly because of excessive recursion. Failure to allocate resources for a new thread can instead produce OutOfMemoryError.

Can Java garbage-collect objects that reference each other?

Yes. A cycle of ordinary object references doesn't prevent collection when no path from a GC root reaches any object in the cycle.

Can the heap lead to memory leaks?

Yes. Objects that your application no longer needs can remain reachable through static collections, caches, or other live references, preventing their reclamation. Garbage collection tracks reachability, not whether retaining an object is useful.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me