Profiling memory
Memory problems come in two flavors that look identical from the outside: you
keep objects you should have dropped, or you allocate and free at a furious
rate that the collector cannot keep up with. Total usage tells you something
is wrong; only an allocation profile tells you which line to change.
Method
- Decide first: live growth or allocation rate. A heap that climbs and
never falls is retention. A flat heap with high garbage-collection CPU is
churn: short-lived objects born and killed in a loop. Watch RSS over time
and the GC pause log to tell them apart before choosing a tool.
- For retention, take two heap snapshots and diff them. Capture one
after warmup and one after the leak has grown:
jmap -dump then Eclipse
MAT for the JVM, tracemalloc.take_snapshot().compare_to(...) in Python,
Chrome DevTools heap snapshots for Node. The diff names the object type and
the retaining path that is holding it alive.
- For churn, run an allocation profiler that keeps stacks.
memray run app.py for Python, async-profiler -e alloc for the JVM, heaptrack ./app for native code. These sample allocations with the call stack, so
the flame graph shows which call site allocated the bytes, not just how
many.
- Rank by allocation count, not only by size. A million tiny objects
costs more in GC pressure than one large buffer of equal bytes. Sort the
profile by number of allocations to find the loop creating temporaries:
boxed integers, intermediate lists, format strings, defensive copies.
- Follow the dominator tree to the real owner. In a snapshot, the object
at the top of memory is often innocent; the dominator, the single node
whose removal frees the whole subtree, is the cache, listener list, or
static map that never releases. Break that one reference and the subtree
collapses.
- Fix the site, then confirm the curve flattens. Pool the buffer, reuse
the object, drop the stale reference, or bound the cache. Rerun the same
workload and watch RSS plateau or GC time fall; if the curve still climbs,
the diff pointed at a symptom and a second retainer remains.
Signals
- Can you name the type and the call site responsible for the most bytes?
- Does the retaining path lead to something you can legitimately release?
- After the fix, does RSS reach a steady state under the same load?
Boundaries
Profilers attribute managed and instrumented allocations; memory lost by a C
extension or a raw mmap outside the runtime stays invisible, and native
leak detection (Valgrind, ASan, LeakSanitizer) covers that ground. This skill
locates allocation; whether an object should live that long is design.
1---2name: profiling-memory3description: Attribute memory growth and allocation churn to the exact call sites that produce it using an allocation profiler. Use when a process grows without bound, spends too much time in garbage collection, or allocates far more than its working set explains.4---56# Profiling memory78Memory problems come in two flavors that look identical from the outside: you9keep objects you should have dropped, or you allocate and free at a furious10rate that the collector cannot keep up with. Total usage tells you something11is wrong; only an allocation profile tells you which line to change.1213## Method14151. **Decide first: live growth or allocation rate.** A heap that climbs and16 never falls is retention. A flat heap with high garbage-collection CPU is17 churn: short-lived objects born and killed in a loop. Watch RSS over time18 and the GC pause log to tell them apart before choosing a tool.192. **For retention, take two heap snapshots and diff them.** Capture one20 after warmup and one after the leak has grown: `jmap -dump` then Eclipse21 MAT for the JVM, `tracemalloc.take_snapshot().compare_to(...)` in Python,22 Chrome DevTools heap snapshots for Node. The diff names the object type and23 the retaining path that is holding it alive.243. **For churn, run an allocation profiler that keeps stacks.** `memray run25 app.py` for Python, `async-profiler -e alloc` for the JVM, `heaptrack26 ./app` for native code. These sample allocations with the call stack, so27 the flame graph shows which call site allocated the bytes, not just how28 many.294. **Rank by allocation count, not only by size.** A million tiny objects30 costs more in GC pressure than one large buffer of equal bytes. Sort the31 profile by number of allocations to find the loop creating temporaries:32 boxed integers, intermediate lists, format strings, defensive copies.335. **Follow the dominator tree to the real owner.** In a snapshot, the object34 at the top of memory is often innocent; the dominator, the single node35 whose removal frees the whole subtree, is the cache, listener list, or36 static map that never releases. Break that one reference and the subtree37 collapses.386. **Fix the site, then confirm the curve flattens.** Pool the buffer, reuse39 the object, drop the stale reference, or bound the cache. Rerun the same40 workload and watch RSS plateau or GC time fall; if the curve still climbs,41 the diff pointed at a symptom and a second retainer remains.4243## Signals4445- Can you name the type and the call site responsible for the most bytes?46- Does the retaining path lead to something you can legitimately release?47- After the fix, does RSS reach a steady state under the same load?4849## Boundaries5051Profilers attribute managed and instrumented allocations; memory lost by a C52extension or a raw `mmap` outside the runtime stays invisible, and native53leak detection (Valgrind, ASan, LeakSanitizer) covers that ground. This skill54locates allocation; whether an object should live that long is design.