C++ Memory Leaks — 1.5 GB After 50 Tab Cycles
RSS grew to 1.
- Memory leak: allocated heap memory that is never freed
- Detection tools: Valgrind (memcheck), AddressSanitizer (ASan)
- Prevention: RAII, smart pointers (unique_ptr, shared_ptr, weak_ptr)
- Performance impact: RSS growth ~1-10 MB/hour typical, OOM kill at 3-5 GB
- Production gotcha: leak detection tools add 2-10x slowdown, disable in production
- Biggest mistake: relying on manual new/delete instead of RAII wrappers
Imagine you're renting storage units. Every time you need to store something, you rent a new unit. A memory leak is when you forget to return the key after you've emptied the unit — the unit stays rented, the bill keeps growing, and eventually you run out of storage units entirely. In C++, your program rents memory from the heap using 'new' and must return it using 'delete'. When it forgets to return that memory, it leaks — silently, invisibly — until your server crashes at 3am.
Memory leaks are the silent killers of C++ applications. Unlike a crash that screams at you immediately, a memory leak is a slow bleed — your process's RSS (Resident Set Size) climbs steadily over hours or days, swap starts thrashing, and eventually the OOM killer on Linux terminates your process with no warning. This is exactly what happened in a famous 2015 Firefox bug where tab-related allocations were never freed, causing hundreds of megabytes of leak per browsing session. The C++ runtime has no garbage collector watching your back. Every byte you allocate is your responsibility to free.
The core problem is the gap between WHEN you allocate memory and WHEN you need to free it. Throw exceptions, early returns, complex ownership semantics, or circular references into the mix, and that gap becomes a minefield. Raw pointers in C++ carry no metadata about who owns them, who should free them, or whether they're still valid. The language gives you a chainsaw — powerful, but it doesn't stop you from cutting your own hand off.
By the end of this article, you'll understand exactly how the heap allocator works under the hood, why leaks happen even in 'careful' code, how to use Valgrind and AddressSanitizer to pinpoint leaks down to the exact line, and how to architect your code with RAII and smart pointers so that leaks become structurally impossible. You'll also leave with the exact vocabulary and depth to ace memory management questions in senior C++ interviews.
What Is a Memory Leak and Why Does It Happen?
A memory leak occurs when a program allocates memory from the heap (using new, malloc, or calloc) and never deallocates it. The leaked memory remains allocated until the process exits. The operating system reclaims it on process termination, but during the process lifetime, the leak gradually reduces available heap space.
Leaks happen because C++ does not have automatic garbage collection. The programmer must explicitly pair every allocation with a deallocation. Real-world causes include:
- Exception thrown before
deleteis reached. - Early return paths that skip cleanup.
- Overwriting the only pointer to allocated memory (pointer reassignment without delete).
- Circular references involving
shared_ptr. - Missing virtual destructors, causing derived resource not to be released.
- Every allocation grabs a contiguous block from the free list.
- Every deallocation returns the block to the free list (coalescing neighbors).
- A leak means the block stays in use but is no longer reachable — the free list shrinks permanently.
- Fragmentation makes the problem worse: small leaks fragment the heap, causing future allocations to fail even if total free memory is large.
new must be paired with a delete or ownership transferred to a smart pointer.Heap Allocator Internals: How Leaks Steal Memory
The heap allocator (glibc's malloc, tcmalloc, jemalloc) manages a pool of memory blocks. When you call new, it finds a free block via the free list, marks it as used, and returns the address. When you call delete, it returns the block to the free list. Coalescing merges adjacent free blocks into larger ones.
A leak does not just lose memory — it also fragments the heap. If many small blocks are leaked in the middle of the heap, the free list contains many small holes. A request for a larger contiguous block may fail even though total free memory is sufficient. This is called external fragmentation.
Production allocators also maintain thread caches to reduce lock contention. Leaked memory in thread caches is even harder to track because it may appear as "in use" but not actually reachable by any pointer. Tools like Valgrind intercept every malloc/free call to track reachable pointers.
--enable-prof to track fragmentation.RAII and Smart Pointers: Making Leaks Impossible
RAII (Resource Acquisition Is Initialization) ties resource lifetime to object lifetime. When an object goes out of scope, its destructor automatically frees the resource. This makes leaks structurally impossible if the resource is owned by an RAII wrapper.
std::unique_ptr: exclusive ownership, no copy, move-only. Leak-free because the destructor callsdelete.std::shared_ptr: shared ownership via reference counting. Leak only if circular references exist.std::weak_ptr: non-owning observer that breaks reference cycles.
Rule: Never use raw new/delete outside of a RAII wrapper or low-level allocator. The only exceptions are when writing custom containers or interfacing with C libraries.
- Destructors are called for stack unwinding on exceptions, returns, and normal scope exit.
- No manual cleanup needed — reducing cognitive load.
- unique_ptr is zero-overhead over raw pointer in release builds.
- shared_ptr has reference counting overhead (atomic increments) but often acceptable.
- weak_ptr breaks cycles: use when two objects reference each other.
shared_ptr in a graph-like structure where nodes hold pointers to each other. This creates a reference cycle. Neither node's reference count drops to zero and both leak.weak_ptr for one direction of the cycle.Using Valgrind and AddressSanitizer to Find Leaks
Two dominant tools for finding memory leaks:
Valgrind (memcheck): Runs the program under a synthetic CPU. Intercepts all malloc/free/new/delete calls. Tracks reachability by scanning memory for pointer values. Reports: - definitely lost: pointer to block overwritten, no way to free. - indirectly lost: block is only referenced by other lost blocks. - possibly lost: pointer to interior of block exists, but start address might be lost. - still reachable: pointer still exists but block not freed before exit (technically not a leak, but often suggests design issue).
AddressSanitizer (ASan): Compile-time instrumentation. For every allocation, it records a shadow byte that tracks access permissions. At runtime, it detects heap-use-after-free, buffer overflow, and memory leaks (with detect_leaks=1). Much faster than Valgrind (2-4x slowdown vs 10-50x).
Both tools should be used during development and in stress testing. They are too slow for production. Use core dumps and heap profiling in production.
Production Debugging Without Tools: Core Dumps and Heap Analysis
When a process is OOM-killed, you lose the process. But if you enable core dumps (ulimit -c unlimited), the OS saves the entire memory image to a file. You can analyze it post-mortem: - Use gdb to inspect pointers and see what blocks are reachable. - Use pstack to get thread stacks. - Use malloc_info(0, stderr) to dump glibc's internal allocation statistics.
Heap profiling tools like gperftools (Google Performance Tools) or jemalloc's built-in profiler can be enabled at runtime with minimal overhead (~5%). They generate heap snapshots that you can diff over time to find growing allocations.
In production, never run Valgrind or ASan. Instead, enable lightweight heap profiling continuously. Set a threshold: if RSS grows more than 10% in an hour, alert the pager with a heap snapshot attached.
coredumpctl (systemd) to compress and manage cores. Alternatively, limit by ulimit -c 2097152 (2 GB max).Common Leak Patterns and How to Fix Them
- Missing virtual destructor: Base class pointer to derived object. If base destructor is not virtual, derived destructor never runs, leaking derived members.
- Circular shared_ptr references: A->B and B->A both hold shared_ptr. Neither reference count reaches zero. Fix: use weak_ptr in one direction.
- Exception in constructor: If constructor throws after
newbut before assigning to a smart pointer, the raw allocation is leaked. Fix: use RAII wrappers for all allocations in constructor initializer lists. - Unhandled allocation failure:
newmay throwstd::bad_alloc. If not caught, the code after it may never execute, potentially leaking prior allocations. Fix: always handle exceptions or use nothrow new. - Forgetting to delete in setter/reassignment:
ptr = new X;overwrites old pointer without delete. Fix: always delete before reassigning, or use smart pointers. - Thread-local storage leaks: If a thread exits without cleaning up its TLS blocks, those blocks remain allocated until process death. Fix: register cleanup functions with
pthread_key_createdestructor.
Static Analysis and Coding Standards: Prevent Leaks at Compile Time
The best tool for memory leaks is prevention. C++ has evolved to make leaks harder to write: - -Wall -Wextra -Werror flags catch some cases. - clang-tidy with cppcoreguidelines-no-malloc, modernize-use-auto, modernize-use-equals-default. - cppcheck for static analysis: detects missing delete, mismatched allocation/deallocation, leaky containers. - -fsanitize=address as part of CI pipeline.
- Never use
new/deletedirectly (exception: custom allocators). - Use
std::make_uniqueandstd::make_shared. - Use RAII wrapper for every resource (file handle, socket, mutex).
- Prefer containers over raw arrays.
- Use C++17's
std::shared_ptrwithstd::make_sharedfor exception safety. - Mark classes with
finalif not intended for inheritance to avoid virtual destructor issues.
clang-tidy and cppcheck to your code review CI. Reject PRs that introduce any new new or delete outside of low-level modules.cppcoreguidelines checks and banning raw new/delete in code reviews, the leak rate dropped to zero within one release cycle.The Firefox Tab Leak That Ate 400 MB Per Session
- Never assume GC handles circular references across C++/JavaScript boundaries.
- Always add explicit destructors or cleanup handlers for long-lived objects.
- Profile RSS growth over hours, not minutes — small leaks compound to megabytes.
- Use heap profiling tools early in development; retrofitting is expensive.
Key takeaways
Common mistakes to avoid
5 patternsForgetting to delete in early return paths
Missing virtual destructor in base class
override and = default in derived. Enable -Wsuggest-override compiler flag.Circular shared_ptr references
Reassigning a pointer without deleting the old value
Assuming memory is freed on thread exit without TLS cleanup
pthread_setspecific may leak if the thread exits without calling pthread_setspecific(key, NULL) first.pthread_key_create that frees the TLS block. Or use thread_local variables with RAII objects.Interview Questions on This Topic
Explain the difference between a memory leak and memory fragmentation. How would you diagnose each in a production system?
malloc_info(0, stderr) for glibc). Use mallctl for jemalloc. Look for many small free blocks. Also monitor RSS vs. virtual memory (VSZ) — large VSZ with moderate RSS often indicates fragmentation.
Fix: Leaks are fixed by adding missing deletes or using RAII. Fragmentation is mitigated by using memory pools, slab allocators, or jemalloc's background coalescing.Frequently Asked Questions
That's C++ Advanced. Mark it forged?
5 min read · try the examples if you haven't