Memory leaks explained
What a memory leak actually is, why they happen even in garbage-collected languages, and how to find and prevent them.
What is a memory leak?
A memory leak is memory a program has allocated but can no longer use and never releases. The memory stays reserved for the life of the process, so usage climbs over time until the program slows, gets killed by the OS, or crashes with an out-of-memory error. Leaks are especially dangerous in long-running processes — servers, daemons, mobile apps — where a slow drip eventually exhausts everything.
Two worlds: manual vs garbage-collected
In unmanaged languages (C, C++) you allocate and free memory yourself. A leak is the simple
case: you malloc/new and never free/delete — often on an
error path that skips the cleanup, or because ownership of a pointer was unclear.
In garbage-collected languages (Java, C#, JavaScript, Python, Go) the runtime frees memory that is no longer reachable. But the GC can only collect what nothing references — so a leak here is really an unintended reference: something still points at an object you’re done with, so the GC keeps it alive forever. “Garbage collected” does not mean “leak-proof.”
Common causes
- Unfreed allocations (manual languages) — early returns and exceptions that skip the free.
- Growing collections — a cache, list, or map you keep adding to but never evict from. The single most common leak in managed languages.
- Forgotten event listeners / callbacks — registering a handler and never removing it keeps the listener (and everything it closes over) alive. Common in UI and browser code.
- Closures capturing too much — a closure that captures a large object keeps it referenced for as long as the closure lives.
- Static / global references — anything hung off a global or static field never becomes unreachable on its own.
- Unclosed resources — file handles, sockets, and DB connections leak their backing memory (and OS handles) if never closed.
How to detect them
- Watch the trend: a leak shows as memory that rises and never comes back down across a steady workload. Graph process memory over time first — confirm there’s actually a leak.
- Heap snapshots / profilers: capture two snapshots and diff them — objects that keep growing between them are your suspects. (Browser DevTools, Java’s VisualVM/heap dumps, .NET memory profilers, Go’s pprof.)
- Leak detectors for native code: Valgrind, AddressSanitizer (ASan), and similar tools flag allocations that were never freed.
How to prevent them
- Tie lifetime to scope: RAII / smart pointers in C++,
using/try-with-resourcesin C#/Java, context managers in Python — release on scope exit, even on errors. - Always unregister what you register — pair every listener add with a remove.
- Bound your caches — set a max size or TTL and evict; use weak references for caches that should not keep objects alive on their own.
- Be deliberate with global/static state — it lives forever by definition.
FAQ
Can a garbage-collected language leak memory?
Yes. The GC only frees unreachable objects, so any lingering reference — a growing cache, a forgotten listener, a static field — keeps memory alive indefinitely.
What’s the most common leak?
An unbounded collection — a cache or list you keep adding to but never clear. It looks innocent and grows quietly under load.
How do I confirm it’s a leak and not normal usage?
Run a steady, repeating workload and graph memory over time. Normal usage plateaus; a leak keeps climbing and never returns to baseline.
Related
See multithreading & concurrency and OOP concepts, or browse the full Reference.