Java Tutorial
🔍

Java Heap Memory

Java Heap Memory

Every object you create with new in Java ends up in one place: the heap. It is the single largest, shared region of memory the JVM manages, and it is also the region garbage collection spends nearly all of its time cleaning up. Understanding how objects actually get placed in the heap, and how long they tend to stay there, is what separates a developer who can explain an OutOfMemoryError: Java heap space crash from one who just restarts the service and hopes it does not happen again.

What Is Heap Memory?

The heap is a region of memory the JVM reserves when it starts, shared across every thread in the application, where every object and array actually lives once created. Unlike stack memory, covered in this series' dedicated Stack Memory article, which is torn down automatically the instant a method returns, the heap has no such per-call cleanup — an object stays on the heap for as long as something, somewhere in the program, still holds a reference to it.

How Heap Memory Works Internally

Modern JVMs do not treat the heap as one undivided pool. HotSpot, the JVM implementation shipped with most JDK distributions, organizes it around the generational hypothesis — the well-documented, empirical observation that most objects die young, while a small number of objects survive to live a long time. Structuring the heap around that pattern lets the collector spend most of its effort on the cheap, common case instead of scanning the entire heap on every pass.

One sentence before the diagram: new objects are always allocated in the young generation first, and only get promoted to the old generation after surviving several collection cycles.

Heap Memory
  |
  +-- Young Generation
  |     +-- Eden Space         (new objects allocated here first)
  |     +-- Survivor Space S0
  |     +-- Survivor Space S1  (objects that survive a minor GC move here)
  |
  +-- Old Generation (Tenured)
        (long-lived objects promoted after surviving
         several minor GC cycles)

Eden is where nearly every object's life begins. When a minor garbage collection runs, whatever in Eden is still reachable moves into one of the two survivor spaces; objects that keep surviving repeated minor collections eventually get promoted into the old generation, which is collected far less often but at greater cost per collection. This entire promotion mechanism, and how collection actually reclaims memory, is covered in full in this series' dedicated Garbage Collection article — this article's focus is on where objects live and how they get placed there.

An object becomes eligible for collection the moment nothing reachable from a GC root — a local variable on any thread's stack, a static field, an active thread itself — still points to it. Reachability, not scope, is what actually determines an object's lifetime on the heap.

Heap memory is shared across every thread in the JVM, in direct contrast to stack memory, where each thread gets its own private stack. That sharing is exactly why objects on the heap need synchronization when multiple threads touch them concurrently, while a thread's own local primitives never do. The overall heap size is configurable at startup with the -Xms (initial size) and -Xmx (maximum size) flags, rather than left entirely to a fixed default.

Code Examples

Two separately constructed objects with identical field values are still two distinct objects on the heap, which is easy to say but worth actually verifying.

1// File: HeapIdentityExample.java 2 3public class HeapIdentityExample { 4 5 static class Customer { 6 String name; 7 8 Customer(String name) { 9 this.name = name; 10 } 11 } 12 13 public static void main(String[] args) { 14 Customer first = new Customer("Ananya"); 15 Customer second = new Customer("Ananya"); 16 17 System.out.println("Same field value: " + first.name.equals(second.name)); 18 System.out.println("Same heap object: " + (first == second)); 19 } 20}
Output:
Same field value: true
Same heap object: false

first and second hold equal data, but each new Customer(...) call allocated its own separate space on the heap, so == — which compares references, not content — correctly reports them as different objects. The reverse is just as important to see directly: when two variables reference the very same heap object, a change made through one is visible through the other.

1// File: SharedHeapReferenceExample.java 2 3public class SharedHeapReferenceExample { 4 5 static class Cart { 6 int itemCount; 7 } 8 9 static void addItem(Cart cart) { 10 cart.itemCount++; 11 } 12 13 public static void main(String[] args) { 14 Cart cart = new Cart(); 15 Cart sameCart = cart; 16 17 addItem(cart); 18 addItem(sameCart); 19 20 System.out.println("Item count: " + cart.itemCount); 21 System.out.println("Same object seen through both references: " + (cart.itemCount == sameCart.itemCount)); 22 } 23}
Output:
Item count: 2
Same object seen through both references: true

cart and sameCart are two different stack variables, but they hold the identical reference, pointing at one single Cart object on the heap — every increment lands on that one shared object, regardless of which variable was used to reach it.

Real-World Example

A reporting service caches generated report content in memory so a repeat request for the same report does not need to regenerate it from scratch.

1// File: ReportCache.java 2import java.util.*; 3 4public class ReportCache { 5 private final Map<String, String> cachedReports = new HashMap<>(); 6 7 public void store(String reportId, String reportContent) { 8 cachedReports.put(reportId, reportContent); 9 } 10 11 public String get(String reportId) { 12 return cachedReports.get(reportId); 13 } 14 15 public int size() { 16 return cachedReports.size(); 17 } 18}
1// File: ReportCacheDemo.java 2 3public class ReportCacheDemo { 4 public static void main(String[] args) { 5 ReportCache cache = new ReportCache(); 6 7 for (int i = 1; i <= 5; i++) { 8 cache.store("report-" + i, "Monthly summary for report " + i); 9 } 10 11 System.out.println("Cached reports: " + cache.size()); 12 System.out.println("Sample: " + cache.get("report-3")); 13 } 14}
Output:
Cached reports: 5
Sample: Monthly summary for report 3

Every string stored in cachedReports stays reachable for as long as the ReportCache instance itself is reachable, since the map field holds a direct reference to each one. A mistake that appears often in fresher pull requests is treating a HashMap-backed cache exactly like this as free, unlimited storage, since nothing about the code looks dangerous and it runs perfectly fine in local testing with a handful of reports. In production, this precise pattern is one of the most common causes of a slow, days-long climb toward OutOfMemoryError — the cache keeps growing because nothing ever calls remove(), and every cached object remains reachable, so garbage collection has no legitimate reason to ever reclaim it. Teams following clean architecture will typically cap a cache like this with an eviction policy, a fixed size limit, a time-based expiry, or a proper library built for the job, rather than relying on an unbounded map and hoping traffic stays low.

Best Practices

Keep request-scoped or otherwise transient data short-lived wherever reasonably possible, so most allocations stay confined to the young generation and get collected cheaply.

Bound every long-lived collection explicitly with an eviction policy — a maximum size, a time-based expiry, or both — rather than trusting that traffic will stay low enough for an unbounded structure to be safe.

Set -Xms and -Xmx deliberately for a production service instead of trusting JVM defaults, since default heap sizing is derived from the machine's available memory and can behave very differently across environments.

Favor immutable, reusable objects over throwaway copies created in tight loops where it genuinely makes sense, since fewer allocations mean less pressure on the young generation.

Common Mistakes

Assuming that setting a reference to null frees the object immediately is one of the most persistent misconceptions about the heap. It does not — it only removes that one path to reach the object. The object remains fully intact on the heap until garbage collection later determines that nothing reachable anywhere in the program still points to it, and reclaims it then.

A static field silently becoming a permanent GC root is a second, subtler version of the same problem this article's cache example demonstrates, but it can happen in code that is not "a cache" at all. A static List left in for debugging and never removed, or an event-listener registry that never unregisters a listener, keeps everything it references reachable for the application's entire lifetime, since a static field's own lifetime is tied to the class itself, not to any particular request or method call.

Interview Questions

Q1. What is heap memory in Java, and what gets stored there?

The heap is the runtime memory region where every object and array created with new lives, shared across all threads and managed by the garbage collector. Interviewers are listening for whether you know it is shared, not per-thread, and that it holds objects specifically, not local primitives.

Q2. What is the generational hypothesis, and how does it shape the heap's structure?

It is the well-documented observation that most objects die young while a small number live a long time, and HotSpot's heap is split into a young generation, collected frequently and cheaply, and an old generation, collected less often but at greater cost, specifically to exploit that pattern. The nuance interviewers listen for is whether you can explain why this split exists, not just recite that it does.

Q3. What is the difference between the young generation and the old generation?

The young generation, made up of Eden and two survivor spaces, holds newly allocated and mostly short-lived objects. Objects that survive several minor garbage collection cycles get promoted into the old generation, which is collected less frequently. Interviewers listen for whether you name promotion as the mechanism connecting the two, rather than describing them as two independently managed pools.

Q4. What happens to an object when a reference to it is set to null?

Nothing immediate happens to the object itself — setting a reference to null only removes that one path to reach it. The object stays fully intact on the heap until garbage collection later determines it is unreachable from anywhere in the program and reclaims it. This exact distinction between unreachable and immediately freed is what separates a candidate who has actually reasoned about garbage collection from one who has memorized a slogan.

Q5. Is heap memory shared across threads, or does each thread get its own?

It is shared across every thread in the JVM, unlike stack memory, where each thread gets its own private stack. Interviewers use this question to check whether you connect it directly to why thread safety matters at all for shared objects.

Q6. What causes java.lang.OutOfMemoryError: Java heap space, and how would you begin diagnosing it in production?

It means the JVM could not allocate a new object because the heap is full and garbage collection could not free enough space, typically because too many objects remain reachable. The real diagnostic path in production is a heap dump analyzed with a tool like Eclipse MAT, looking for which object type is retained in unexpectedly large numbers and tracing back what is holding references to all of them. Interviewers want to hear "reachable, not orphaned" and "heap dump analysis," not "just increase -Xmx and move on."

Q7. Can heap memory size be configured, and what happens if it is set too small?

Yes, through -Xms for the initial size and -Xmx for the maximum size. A heap set too small forces more frequent garbage collection as the JVM works harder to reclaim space, and can eventually cause OutOfMemoryError even in an application with no genuine leak, simply because there is not enough room for its legitimate working set. Interviewers want you to distinguish an undersized heap from an actual leak, since both produce similar symptoms.

FAQs

Is heap memory the same as RAM?

No. Heap memory is a region within whatever memory the JVM process has been allocated, managed according to the -Xms and -Xmx settings, not the entirety of a machine's physical RAM.

Does every object in Java live in the heap?

Nearly every object does. Local variables — whether primitive values or object references — live on the stack, while the actual object data an object reference points to lives on the heap.

What happens when the heap runs out of space?

The garbage collector runs to try to reclaim space by removing unreachable objects, and if it still cannot free enough space, the JVM throws OutOfMemoryError: Java heap space, after which the application typically cannot continue normally.

Can I see how much heap memory my application is using?

Yes, tools like VisualVM, JConsole, or GC logging flags can report heap usage over time, though the exact figures vary by machine, workload, and JVM version and are not worth memorizing as fixed facts.

Is a larger heap always better for performance?

Not necessarily. A larger heap can mean less frequent garbage collection, but when a full collection does eventually run on a very large heap, it can take noticeably longer to complete simply because there is more memory to scan. Sizing is a genuine tradeoff, not something to maximize blindly.

What is the difference between heap memory and Metaspace?

Metaspace, covered in this series' dedicated Metaspace vs PermGen article, stores class metadata — the structure of loaded classes themselves — not the objects created from those classes. Heap memory stores the actual object instances.

Does garbage collection pause my application while it runs?

Some garbage collection phases do pause application threads, though how long and how often depends heavily on which collector is in use and how the heap is tuned — this is covered in depth in this series' dedicated Garbage Collection article.

Summary

Heap memory is where every object you create actually lives, organized by the JVM into generations specifically because most objects turn out to be short-lived. Understanding that structure is what turns a vague "the app is using too much memory" complaint into an actual diagnosis — a real leak from objects still reachable somewhere, or simply a heap sized too small for legitimate load.

The cache example in this article is the pattern worth remembering above everything else here: nothing about a growing HashMap looks dangerous in code review, which is exactly why it is worth looking for specifically.

What to Read Next