Memory Profiling: tracemalloc, Reference Trees & Leak Hunting
Diagnosing memory leaks and high Resident Set Size (RSS) usage in Python requires specialized memory profiling tools. Because Python manages memory via Reference Counting and Generational GC, leaks typically manifest as un-collected circular reference loops, long-lived global container retention, or tracebacks stored in exception objects.
This chapter details memory profiling with tracemalloc, frame memory allocation snapshots, object graph visualization via objgraph, and production leak hunting strategies.
1. Tracing Allocations with tracemalloc
The standard library tracemalloc module traces Python memory allocations by intercepting pymalloc and system malloc() calls, recording the exact file name and line number responsible for every memory allocation.
import tracemalloc
# 1. Start memory tracing
tracemalloc.start(10) # Capture up to 10 stack frames per allocation
# Take initial memory snapshot
snapshot1 = tracemalloc.take_snapshot()
# Run target workload
run_heavy_workload()
# Take second memory snapshot
snapshot2 = tracemalloc.take_snapshot()
# Compare snapshots to identify top memory growth lines!
stats = snapshot2.compare_to(snapshot1, "lineno")
for stat in stats[:5]:
print(stat)Sample Output:
/app/services/cache.py:42: size=14.2 MiB (+14.2 MiB), count=1204 (+1204)
/app/utils/logger.py:18: size=2.1 MiB (+2.1 MiB), count=410 (+410)2. Visualizing Reference Trees with objgraph
When an object is kept alive unexpectedly, use the third-party objgraph library to inspect its incoming and outgoing reference pointers:
import objgraph
# Find objects with the highest instance growth
objgraph.show_most_common_types(limit=5)
# Render a PNG image showing reference chains keeping an object alive!
leaked_objects = objgraph.by_type("CustomSession")
if leaked_objects:
objgraph.show_backrefs(leaked_objects[0], max_depth=5, filename="leak_graph.png")objgraph Reference Backref Tree:
[ Target Object: CustomSession @ 0x7F9A ]
^
| (Referenced by attribute 'session')
[ WorkerTask @ 0x4B2C ]
^
| (Referenced by item index 0)
[ Global List: '_active_tasks' in module 'app.worker' ]3. Common Python Memory Leak Root Causes
- Unbounded Global Containers: Appending objects to global lists or dicts (
CACHE[key] = val) without LRU eviction policies or TTL bounds. - Circular References in Closures: Inner closure functions capturing references to
selfor outer container objects that also hold references to the closure function. - Traceback Retention in Exceptions: Storing
Exceptioninstances in long-lived class attributes (self.last_error = err), keeping all local stack frame variables alive in memory viaerr.__traceback__. - C-Extension Memory Leaks: Memory allocated inside third-party C-extensions via raw
malloc(3)without corresponding C-levelfree(3)calls.
4. Production Memory Leak Hunting Strategy
Production Memory Leak Hunting Workflow:
[ Detect Memory Growth (Process RSS rising continuously in Datadog/Prometheus) ]
|
v
[ Step 1: Capture tracemalloc Snapshots ]
(Compare snapshot at startup vs snapshot post-load to find top allocation lines)
|
v
[ Step 2: Inspect Object Counts via objgraph ]
(Identify class types with unbounded growth using objgraph.show_most_common_types())
|
v
[ Step 3: Trace Backreferences ]
(Render objgraph.show_backrefs() to locate the root global variable or container holding references)
|
v
[ Step 4: Apply Fix ]
(Use weakref, add LRU eviction bounds, or clear exception tracebacks)