Question

How to Create and Understand a Memory Leak in Java

javamemorymemory-leaks

Question

In a Java interview, I was asked how to create a memory leak in Java. I was not sure how to answer because Java has garbage collection, so I did not know how a leak could still occur.

What is a simple example of a memory leak in Java, and why does it happen?

For example, what kind of code would keep using memory unnecessarily instead of allowing it to be garbage-collected?

Short Answer

By the end of this page, you will understand what a memory leak means in Java, why garbage collection does not prevent all leaks, how to create a simple leak example, and how to recognize and avoid the patterns that cause leaks in real applications.

Concept

In Java, a memory leak does not usually mean memory becomes permanently unreachable in the same way it can in manual-memory languages like C or C++. Instead, it usually means:

  • your program keeps references to objects it no longer needs
  • because those references still exist, the garbage collector considers those objects reachable
  • reachable objects are not collected
  • memory usage keeps growing over time

This is the key idea: Java garbage collection only frees objects that are no longer reachable.

If your code accidentally stores objects in a collection, cache, static field, listener list, thread-local, or long-lived object, then those objects stay alive even if your program will never use them again.

A Java memory leak matters because it can cause:

  • gradually increasing heap usage
  • slower garbage collection
  • degraded application performance
  • OutOfMemoryError
  • unstable long-running services

A leak is often not about "forgot to free memory". It is about forgot to release references.

Mental Model

Think of Java memory like a storage room managed by a cleaner.

  • The garbage collector is the cleaner.
  • The cleaner removes boxes only if no one has a label pointing to them.
  • A reference is that label.

If you throw a box into a giant closet and keep the closet key forever, the cleaner sees the box as still owned and does not remove it.

So a Java memory leak is like saying:

"I do not need these boxes anymore, but I accidentally kept them in a closet that never gets emptied."

The problem is not that the cleaner failed. The problem is that your program kept saying the objects were still important.

Take Quiz

Syntax and Examples

A common way to create a memory leak in Java is to keep adding objects to a long-lived collection and never remove them.

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

public class MemoryLeakExample {
    private static final List<byte[]> leak = new ArrayList<>();

    public static void main(String[] args) {
        while (true) {
            leak.add(new byte[1024 * 1024]); // 1 MB
            System.out.println("Stored objects: " + leak.size());
        }
    }
}

Why this leaks

  • leak is a static list
  • static fields usually live for the lifetime of the program
  • every loop adds a new 1 MB byte array
  • the list keeps references to every array
  • because the arrays are still referenced, the garbage collector cannot free them

Eventually, the program will likely throw:

java.lang.OutOfMemoryError: Java heap space

A smaller conceptual example

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

public class UserCache {
    private final List<String> names = new ArrayList<>();

    public void addName(String name) {
        names.add(name);
    }
}

This is not automatically a leak, but it becomes one if:

  • UserCache lives for a long time
  • names are never removed
  • the list grows forever

So a memory leak is often about lifetime mismatch: short-lived data is stored in a long-lived place.

Step by Step Execution

Consider this example:

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

public class Demo {
    public static void main(String[] args) {
        List<String> items = new ArrayList<>();

        items.add("A");
        items.add("B");
        items.add("C");
    }
}

What happens step by step

  1. items is created as a new ArrayList.
  2. The list object now exists on the heap.
  3. "A", "B", and "C" are added to the list.
  4. The list stores references to those values.
  5. When main finishes, items goes out of scope.
  6. If nothing else refers to that list, it becomes unreachable.
  7. The garbage collector can reclaim it later.

Now compare it with this leaking version:

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

public class Demo {
    private static final List<String> items = new ArrayList<>();

    public static void main(String[] args) {
        while (true) {
            items.add(new String("data" + items.size()));
        }
    }
}

Step by step in the leaking version

  1. items is created as a static list.
  2. Because it is static, it is associated with the class and lives as long as the class stays loaded.
  3. The loop keeps creating new strings.
  4. Each string is added to items.
  5. items keeps references to all previous strings.
  6. None of those strings become unreachable.
  7. Garbage collection cannot reclaim them.
  8. Memory usage keeps increasing.
  9. Eventually the heap fills up.

The important point is: the objects are still reachable, so they are not garbage.

Real World Use Cases

Java memory leaks appear in real applications in patterns like these:

Unbounded caches

Map<String, Object> cache = new HashMap<>();

If entries are added forever and never evicted, memory keeps growing.

Event listeners not removed

A UI, messaging system, or framework may keep listener references. If you register listeners and never unregister them, old objects stay alive.

Static collections

private static final List<Session> sessions = new ArrayList<>();

If old sessions are stored forever, the application retains unnecessary objects.

ThreadLocal misuse

If values are placed in ThreadLocal and not cleared in long-running thread pools, those values can remain attached to threads much longer than expected.

Maps keyed by changing objects

Objects stored in maps may never be removed due to incorrect key handling or lifecycle management.

ORM or persistence context growth

In batch jobs, loading thousands of entities into a persistence context without clearing it can consume a large amount of memory.

Logging or request history buffers

Applications sometimes keep full request or error history in memory for debugging. If there is no limit, this becomes a leak pattern.

Take Quiz

Real Codebase Usage

In real projects, developers usually do not write obviously leaking code on purpose. Instead, leaks come from design mistakes.

Common patterns that help prevent leaks

Bounded caches

Use size limits or time-based eviction.

// Conceptually: keep only a limited amount of data

A cache without limits is often just a memory leak with a nicer name.

Guarding object lifetime

Ask:

  • How long should this data live?
  • Who owns it?
  • When should it be removed?

Remove listeners and callbacks

If you add a listener, make sure there is also a removal path.

Clear temporary collections in batch processing

In loops that process large amounts of data, avoid keeping all results in memory unless needed.

Use weak references when appropriate

For some use cases, WeakHashMap or weak references can help avoid retaining objects only because they are used as cache keys or metadata.

Validate cache strategy in code review

Developers often review:

  • whether a cache has max size
  • whether static fields are necessary
  • whether background threads retain references
  • whether objects are removed from registries

Related defensive patterns

  • early cleanup after a task finishes
  • bounded collections instead of unlimited growth
  • error handling that closes resources and unregisters state
  • configuration limits for queues, buffers, and caches

Common Mistakes

Mistake 1: Assuming garbage collection prevents all leaks

Java has garbage collection, but it only removes unreachable objects.

private static final List<Object> list = new ArrayList<>();

If list keeps growing, those objects are still reachable.

Mistake 2: Keeping everything in a static field

Broken pattern:

public class BadStore {
    public static final List<String> DATA = new ArrayList<>();
}

Why it is risky:

  • static data lives a long time
  • many parts of the app can keep adding to it
  • cleanup is often forgotten

Mistake 3: Building an unbounded cache

Broken code:

Map<String, String> cache = new HashMap<>();

public String getValue(String key) {
    return cache.computeIfAbsent(key, k -> loadValue(k));
}

If keys keep changing and nothing is evicted, memory usage will keep growing.

Mistake 4: Forgetting to remove listeners

Broken pattern:

publisher.addListener(myListener);

If myListener belongs to an object that should disappear later, the publisher may keep it alive.

Mistake 5: Confusing high memory usage with a leak

Not all memory growth is a leak.

Examples that are not necessarily leaks:

  • loading a large file intentionally
  • keeping a valid cache with eviction
  • temporary spikes during batch processing

A leak means memory is retained longer than necessary.

Mistake 6: Holding onto large temporary objects

List<byte[]> results = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    results.add(processFile(i));
}

If you only need each result briefly, storing them all may unnecessarily retain memory.

How to avoid these mistakes

  • prefer local variables when possible
  • remove items when no longer needed
  • limit cache size
  • unregister listeners
  • clear ThreadLocal values
  • think carefully about object lifetime

Comparisons

ConceptWhat it meansCan GC fix it automatically?Example
Java memory leakObjects are still reachable but no longer usefulNoGrowing static list
Normal memory useObjects are still reachable and still neededNo, because they are validActive request data
GarbageObjects are unreachableYesLocal object after method ends
OutOfMemoryErrorJVM cannot allocate enough memoryNot alwaysHeap filled by retained objects

Memory leak vs resource leak

TypeDescriptionExample
Memory leakHeap objects remain referenced unnecessarilyObjects kept in a Map forever
Resource leakExternal resource is not closed properlyFile handle, socket, database connection

Static collection vs local collection

PatternLifetimeLeak risk
Local ArrayList inside a methodUsually short-livedLow
static ArrayListOften application-wideHigh if not managed

Strong reference vs weak reference

Reference typeGC behaviorTypical use
Strong referenceObject stays aliveNormal variables and fields
Weak referenceObject may be collected if no strong references remainMetadata, some cache patterns
Take Quiz

Cheat Sheet

Quick definition

A Java memory leak happens when objects are still reachable but no longer needed.

Core rule

  • Reachable object -> not garbage-collected
  • Unreachable object -> eligible for garbage collection

Common causes

  • static collections
  • unbounded caches
  • listeners not removed
  • ThreadLocal values not cleared
  • long-lived maps and lists
  • batch jobs retaining too much data

Classic leak example

private static final List<byte[]> leak = new ArrayList<>();

while (true) {
    leak.add(new byte[1024 * 1024]);
}

Why it leaks

  • the list is long-lived
  • every array is still referenced
  • GC cannot free referenced objects

Signs of a leak

  • memory keeps growing over time
  • GC runs more often
  • application slows down
  • heap dump shows many retained objects
  • OutOfMemoryError

Prevention checklist

  • remove data when finished
  • avoid unnecessary static state
  • bound caches by size or time
  • unregister listeners
  • clear ThreadLocal.remove()
  • review object lifetime carefully

Important distinction

High memory usage is not always a leak. A leak means memory is retained unnecessarily.

FAQ

Can Java really have memory leaks if it has garbage collection?

Yes. Garbage collection only removes unreachable objects. If your program still references objects it no longer needs, they stay in memory.

What is the simplest Java memory leak example?

A static list that keeps growing forever is the classic example.

private static final List<Object> list = new ArrayList<>();

Is an OutOfMemoryError always caused by a memory leak?

No. It can also happen because the program legitimately needs more memory than the JVM heap allows.

Are caches memory leaks?

Not always. A bounded cache with eviction is usually fine. An unbounded cache can become a leak.

Why are static fields often involved in leaks?

Because they usually live for a long time, so anything they reference may also stay alive for a long time.

Can local variables cause memory leaks?

Usually not for long, because they go out of scope. But they can contribute temporarily if they hold large objects longer than needed.

How do developers find Java memory leaks?

They often use heap dumps, profilers, GC logs, and tools like VisualVM, Java Mission Control, or Eclipse MAT.

What is the interview-friendly definition of a Java memory leak?

A Java memory leak is when objects remain reachable and therefore cannot be garbage-collected, even though the program no longer needs them.

Take Quiz

Mini Project

Description

Build a small Java program that demonstrates a memory leak by storing data in a long-lived static collection. This project helps you see that Java can still run out of memory when references are kept unnecessarily, even though garbage collection exists.

Goal

Create a program that repeatedly allocates memory, stores it in a static list, and eventually exhausts the heap because the objects remain reachable.

Requirements

  • Create a Java class with a static collection that lives for the lifetime of the program.
  • Repeatedly allocate new objects inside a loop.
  • Store each new object in the collection instead of letting it be discarded.
  • Print the number of stored objects as the program runs.
  • Make the example simple enough to demonstrate why garbage collection cannot reclaim the objects.
Take Quiz

Keep learning

Related questions

View more