One of the greatest selling points of modern runtimes like Java (JVM), Go, and Node.js (V8) is automatic memory management. Developers no longer need to manually execute malloc() and free().
However, having a Garbage Collector does not mean your application cannot leak memory.
In a garbage-collected language, a "memory leak" is not unreferenced dangling memory. Instead, it is unintentional object retention: objects that your business logic no longer needs, but which remain reachable through the Garbage Collection Root Graph. Over time, the JVM heap or V8 heap fills up, GC pause times soar, and the application crashes with java.lang.OutOfMemoryError or FATAL ERROR: Ineffective mark-compacts near heap limit.
In this article, we break down the most common GC memory leak vectors and how to diagnose them with heap dumps.
How Garbage Collectors Decide What to Free
Garbage collectors traverse memory starting from GC Roots (active thread stacks, static class variables, and global scopes).
[GC Root: Static Map / Active Thread Stack]
|
v
[Live Object A] ---> [Forgotten Object B (Leaked!)] ---> [Large 50MB Buffer]
As long as a reference path exists from any active GC Root to Object B, the garbage collector cannot reclaim it—even if your business code will never touch Object B again.
1. The Static Collection Trap (Java / Kotlin)
The most notorious Java leak is adding objects to static collections without an eviction strategy:
public class SessionCache {
private static final Map<String, UserSession> activeSessions = new HashMap<>();
public static void register(UserSession session) {
activeSessions.put(session.getId(), session);
}
}
The Fix:
Always use bounded caches with Time-To-Live (TTL) eviction, such as Caffeine in Java:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class SafeSessionCache {
private static final Cache<String, UserSession> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();
}
2. ThreadLocal Leaks in Web Application Servers
In servlet containers (Tomcat, Jetty), threads are pooled and reused across thousands of HTTP requests.
If you store data in a ThreadLocal during a request and fail to call .remove():
public class UserContext {
private static final ThreadLocal<RequestContext> context = new ThreadLocal<>();
public static void set(RequestContext ctx) { context.set(ctx); }
public static RequestContext get() { return context.get(); }
public static void clear() { context.remove(); } // VITAL!
}
If clear() is omitted in a finally block:
- The thread returns to the thread pool.
- The thread keeps a strong reference to
RequestContextand its attached objects. - When another user's request is assigned to that thread, it can even cause security data contamination!

Top comments (0)