DEV Community

Skilled Coder
Skilled Coder

Posted on • Originally published at theskilledcoder.com

Java Memory Leaks: What Causes Them and How to Avoid Them

Java logo dripping

Memory leaks in Java aren’t always loud or obvious. Sometimes, they creep in quietly - through static fields, forgotten listeners, or subtle misuse of common classes. These leaks don’t crash your app right away, but they slowly degrade performance, increase memory usage, and lead to unexpected OOM errors in long-running systems.

In this post, we’ll look at some lesser-known but real-world Java memory leak patterns with clean fixes you can apply right away.


1. Using ThreadLocal without removing

In thread pools (e.g., servlet containers), threads live long. If we don't remove the value, it stays in memory forever, even if it's not used again

Code Snippet

Fix:

Code Snippet

.remove() clears the reference tied to the thread, allowing GC to collect the HeavyObject instance after it's used - avoiding memory bloat in long-lived threads.


2. Static Collections Holding Data

Static map never dies = entries stick around forever = slow memory leak as data piles up.

Code Snippet

Solution: Using a proper caching library (like Caffeine) introduces eviction + TTL, meaning old or unused entries are automatically removed, keeping memory usage in check.

Code Snippet


3. Anonymous Inner Classes Holding Outer Class References

Inner classes implicitly hold a reference to the outer class. If the task lives long, it prevents the outer class from being GC'ed - even if the user navigated away.

Code Snippet

Non-static inner classes hold an implicit reference to their outer class. If they outlive the outer class, they can cause a memory leak.

Fix: Use static inner classes or separate classes. A static inner class does not hold an implicit reference to an instance of the outer class. This decoupling ensures that the lifecycle of the inner class is not tied to the outer class, preventing potential memory leaks.

Code Snippet

when the outer object is unused, it’s freed correctly by the GC.


4. Listeners Not Removed

You added a listener but never removed it. So even if the object that registered it is no longer needed, it stays alive because the button holds it.

Code Snippet

Fix: Explicitly removing the listener breaks the reference chain, allowing both the listener and possibly its enclosing object to be garbage collected.

Code Snippet


5. Holding Strong References to ClassLoaders

private static final List<ClassLoader> loaders = new ArrayList<>();
Enter fullscreen mode Exit fullscreen mode

In plugin/reloadable apps, classloaders should be GC'ed after unload. But strong references keep them in memory, causing class metadata and heap leaks.

Fix:

Weak references don’t prevent GC, so once a classloader is unused, it’s eligible for cleanup. You avoid both heap and Metaspace bloat.

List<WeakReference<ClassLoader>> loaders = new ArrayList<>();
Enter fullscreen mode Exit fullscreen mode

6. Unbounded Executor Queues

The default LinkedBlockingQueue used by Executors.newFixedThreadPool() is unbounded. Submitting millions of tasks causes the queue to grow indefinitely, consuming heap.

Code Snippet

Fix: Use a custom ThreadPoolExecutor with a bounded queue:

Code Snippet

The queue has a size cap, preventing runaway heap usage. CallerRunsPolicy throttles the caller instead of leaking memory.


7. JDBC Connections Not Closed Properly

If the connection isn’t closed, it stays alive, leading to connection leaks and memory/resource exhaustion.

Connection conn = dataSource.getConnection();
// do stuff
Enter fullscreen mode Exit fullscreen mode

Fix:

try (Connection conn = dataSource.getConnection()) {
    // use it
}
Enter fullscreen mode Exit fullscreen mode

The try-with-resources block ensures the connection is always closed, even if an exception occurs - preventing resource + memory leaks.


8. Keeping Long References in Logging Context (like MDC)

Code Snippet

MDC uses ThreadLocal internally. If you don’t clear the context, that data lives on in the thread - leaking memory across requests in thread pools.

Fix:

.clear() removes all MDC values tied to the thread, letting memory get released cleanly after request completion.

Code Snippet


Most memory leaks in Java happen not because of complex code, but because of overlooked patterns in everyday usage.

Catch these early, and your app stays healthy. Miss them, and they quietly eat your memory.

If you found these helpful, drop a follow or share — and let me know if you’ve seen any sneaky leaks in the wild.

For more coding related content subscribe to my newsletter and twitter

AWS Q Developer image

Your AI Code Assistant

Implement features, document your code, or refactor your projects.
Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (0)