| name | memory-management |
| description | Diagnose and resolve memory issues in production systems. Outputs heap analysis, leak detection strategies, GC tuning recommendations, and memory-efficient design patterns. |
| argument-hint | ["language/runtime","symptoms","heap size","GC type"] |
| allowed-tools | Read, Write, Bash |
Memory Management
Memory problems manifest as slow leaks, sudden OOM crashes, or GC pauses degrading latency. Fixing them requires understanding allocation patterns, retention paths, and the GC model of your runtime — not just restarting the process.
Process
- Confirm the symptom. Is it a slow leak, sudden OOM, high GC pause, or high steady-state usage? Each has different causes.
- Establish a baseline. Capture heap size, GC frequency, GC pause duration under normal load.
- Take a heap snapshot. Before and after a suspected leak period. Compare object counts and retained sizes.
- Find retention paths. What is holding a reference to the leaking objects? Walk the reference chain from GC roots.
- Fix the root cause. Don't tune GC as a substitute for fixing leaks.
- Tune GC last. After leaks are fixed, tune heap sizing and GC algorithm for your workload.
- Set memory limits explicitly. Always set container/JVM/Node heap limits. Never rely on defaults in production.
Heap Snapshot Analysis
jmap -dump:format=b,file=heap.hprof <pid>
node --inspect app.js
import tracemalloc
tracemalloc.start()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
import _ "net/http/pprof"
go tool pprof -http=:8080 heap.out
Common Leak Patterns
class DataProcessor:
def __init__(self, event_bus):
self._handlers = []
event_bus.on('data', lambda d: self.process(d))
def __init__(self, event_bus):
self._event_bus = event_bus
self._handler = self._on_data
event_bus.on('data', self._handler)
def _on_data(self, data):
self.process(data)
def shutdown(self):
self._event_bus.off('data', self._handler)
cache = {}
from functools import lru_cache
from cachetools import LRUCache, cached
cache = LRUCache(maxsize=1000)
@cached(cache)
def expensive_lookup(key):
return fetch_from_db(key)
threading
_local = threading.local()
():
_local.user_id = user_id
process()
():
_local.user_id = user_id
:
process()
:
_local.user_id
:
():
.children = []
.parent =
weakref
:
():
.children = []
._parent =
():
._parent() ._parent
():
._parent = weakref.ref(node) node
JVM GC Tuning
-XX:+UseG1GC
-Xms4g -Xmx4g
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=45
-XX:+UseZGC
-Xmx16g
-XX:ConcGCThreads=4
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
Node.js Memory Management
function logMemory() {
const used = process.memoryUsage();
console.log({
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
external: `${Math.round(used.external / 1024 / 1024)}MB`,
rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
});
}
const v8 = require('v8');
setInterval(() => {
const stats = v8.getHeapStatistics();
if (stats.used_heap_size > stats.heap_size_limit * 0.85) {
console.warn('Heap usage >85% — possible leak');
}
}, 30000);
() {
rows = db.();
.(rows);
}
() {
stream = db.();
stream.( TransformStream()).(res);
}
Go Memory Profiling
package main
import (
"net/http"
_ "net/http/pprof"
"runtime"
)
func main() {
go http.ListenAndServe(":6060", nil)
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc: %v MB\n", m.Alloc/1024/1024)
fmt.Printf("Sys: %v MB\n", m.Sys/1024/1024)
fmt.Printf("NumGC: %v\n", m.NumGC)
}
func leaky(ch <-chan int) {
go func() {
for v := range ch {
process(v)
}
}()
}
func fixed(ctx context.Context, ch <-chan int) {
go func() {
{
{
<-ctx.Done():
v, ok := <-ch:
!ok { }
process(v)
}
}
}()
}
Memory-Efficient Design Patterns
| Pattern | When to Use | Memory Saving |
|---|
| Object pooling | Frequent allocation/deallocation of same-size objects | Eliminates GC pressure |
| Flyweight | Many objects sharing common state | Share immutable state |
| Lazy loading | Large objects not always needed | Defer until accessed |
| Streaming | Processing large datasets | O(1) instead of O(n) memory |
| Off-heap storage | JVM: large caches that cause GC | Bypass GC entirely |
| Weak references | Caches that should yield under pressure | Auto-eviction by GC |
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| Infinite in-memory cache | Grows until OOM | LRU/TTL eviction |
| Unsubscribed listeners | Event handlers hold object graphs alive | Explicit lifecycle/teardown |
| Large object graphs in sessions | HTTP sessions retaining megabytes per user | Store only session IDs; fetch from cache |
| Heap dumps in prod without trigger | Performance hit, disk fill | Only on OOM or explicit operator trigger |
| GC tuning before fixing leaks | Masks root cause | Fix leaks first |
| Unbounded queues | Worker queues grow under load | Bounded queues with backpressure |
| String interning abuse | Interned strings never GC'd | Only intern truly global constants |
10 Rules
- Always set explicit memory limits — never rely on OS defaults in production.
- Heap growth between GC cycles is a leak. Heap growth within a cycle is normal allocation.
- Fix leaks before tuning GC. GC tuning on a leaky app is rearranging deck chairs.
- Profile under production-representative load — synthetic tests undercount real leaks.
- Stream large datasets; never buffer them in memory.
- Weak references are the right tool for caches. Strong references keep objects alive forever.
- Always dispose: close streams, deregister listeners, cancel timers.
- Pool expensive objects (DB connections, threads, buffers) — create once, reuse many times.
- Set heap dump on OOM in every JVM process — you need the evidence post-mortem.
- Memory and latency are linked — high GC pause = latency spike. Monitor both together.