Memory Leak
A memory leak retains memory that is never released, so usage climbs until the process thrashes or crashes, often correlated with uptime. Fix root retention with bounded caches, weak references, and deterministic disposal, and catch regressions with soak tests.
A memory leak is memory that an application allocates and then can no longer use but also never frees, because a live reference to it is retained somewhere unintentionally. In garbage-collected languages this is "unintentional retention": the object is reachable, so the collector keeps it forever. In manually managed languages it is allocation without a matching free. Either way, memory usage trends upward over time even when the workload is steady.
Why It Happens
The usual culprits are collections that only ever grow — a static map used as a cache with no eviction, a list of listeners that are added but never removed, a session store keyed by something unbounded. Event subscriptions, callbacks, and closures capture objects and keep them alive. Resources (streams, connections, native handles) are opened and not closed in error paths. Thread-locals retained by a thread pool outlive the request. None of these show up in short tests; the leak only matters over long uptime.
Why It Hurts
Memory rises until the process hits its limit, then it either crashes with OutOfMemory/OOMKilled or grinds to a halt as the garbage collector runs constantly trying to reclaim nothing (GC thrash), spending most CPU on collection and little on work. In containers the orchestrator kills and restarts the pod, causing periodic outages and lost in-flight state. Performance degrades gradually, making the cause hard to spot until it is severe.
Warning Signs
- Heap or RSS that climbs steadily and never returns to a baseline after load subsides.
- OOM kills or crashes correlated with uptime rather than traffic spikes.
- Rising GC frequency and pause time; throughput decaying over hours or days.
- Ever-growing collections, listener lists, or caches with no eviction.
Better Alternatives
- Bounded caches with size/TTL eviction (e.g., LRU) instead of unbounded maps.
- Weak / soft references for caches and listener registries so the GC can reclaim entries.
- Scoped lifetimes and deterministic disposal:
using/try-with-resources/RAII/deferto release resources on every path. - Unsubscribe / remove-listener symmetry: every register has a matching deregister.
- Object pooling done correctly with clear ownership, not indefinite retention.
How to Refactor Out of It
Reproduce the growth, then capture heap snapshots over time and diff them to find which object types accumulate and what holds them (the GC retention path). Fix the root retention: add eviction to caches, remove listeners when their owner dies, close resources in finally/defer/RAII, and clear thread-locals after each task in pooled threads. Replace strong references in caches with weak ones where appropriate. Add leak detection to CI (run a soak test and assert the heap stabilizes) and monitor memory and GC metrics in production so future leaks are caught before an OOM.