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.
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
leakis astaticliststaticfields 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:
UserCachelives 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
itemsis created as a newArrayList.- The list object now exists on the heap.
"A","B", and"C"are added to the list.- The list stores references to those values.
- When
mainfinishes,itemsgoes out of scope. - If nothing else refers to that list, it becomes unreachable.
- 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
itemsis created as astaticlist.- Because it is
static, it is associated with the class and lives as long as the class stays loaded. - The loop keeps creating new strings.
- Each string is added to
items. itemskeeps references to all previous strings.- None of those strings become unreachable.
- Garbage collection cannot reclaim them.
- Memory usage keeps increasing.
- 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.
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
ThreadLocalvalues - think carefully about object lifetime
Comparisons
| Concept | What it means | Can GC fix it automatically? | Example |
|---|---|---|---|
| Java memory leak | Objects are still reachable but no longer useful | No | Growing static list |
| Normal memory use | Objects are still reachable and still needed | No, because they are valid | Active request data |
| Garbage | Objects are unreachable | Yes | Local object after method ends |
| OutOfMemoryError | JVM cannot allocate enough memory | Not always | Heap filled by retained objects |
Memory leak vs resource leak
| Type | Description | Example |
|---|---|---|
| Memory leak | Heap objects remain referenced unnecessarily | Objects kept in a Map forever |
| Resource leak | External resource is not closed properly | File handle, socket, database connection |
Static collection vs local collection
| Pattern | Lifetime | Leak risk |
|---|---|---|
Local ArrayList inside a method | Usually short-lived | Low |
static ArrayList | Often application-wide | High if not managed |
Strong reference vs weak reference
| Reference type | GC behavior | Typical use |
|---|---|---|
| Strong reference | Object stays alive | Normal variables and fields |
| Weak reference | Object may be collected if no strong references remain | Metadata, some cache patterns |
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
staticcollections- unbounded caches
- listeners not removed
ThreadLocalvalues 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
staticstate - 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.
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.
Keep learning
Related questions
Add External JAR Files to an IntelliJ IDEA Java Project
Learn how to add external JAR dependencies to an IntelliJ IDEA Java project using module libraries, and when to use Maven or Gradle instead.
Avoiding Java Code in JSP with JSP 2: EL and JSTL Explained
Learn how to avoid Java scriptlets in JSP 2 using Expression Language and JSTL, with examples, best practices, and common mistakes.
Call a Method After a Delay in Android Java
Learn how to run Java code after a delay in Android using Handler.postDelayed, manage the main thread, and cancel callbacks safely.