11 KiB
11 KiB
Garbage Collection Guide
Understanding and tuning garbage collectors in Node.js (V8) and Python for optimal memory management.
V8 Garbage Collector (Node.js)
Heap Structure
Two Generations:
┌─────────────────────────────────────────────────────────┐
│ V8 Heap │
├─────────────────────────────────────────────────────────┤
│ New Space (Young Generation) - 8MB-32MB │
│ ┌─────────────┬─────────────┐ │
│ │ From-Space │ To-Space │ ← Minor GC (Scavenge) │
│ └─────────────┴─────────────┘ │
│ │
│ Old Space (Old Generation) - Remaining heap │
│ ┌──────────────────────────────────────┐ │
│ │ Long-lived objects │ ← Major GC │
│ │ (survived 2+ Minor GCs) │ (Mark-Sweep)│
│ └──────────────────────────────────────┘ │
│ │
│ Large Object Space - Objects >512KB │
└─────────────────────────────────────────────────────────┘
GC Types:
- Scavenge (Minor GC): Fast (~1ms), clears new space, runs frequently
- Mark-Sweep (Major GC): Slow (100-500ms), clears old space, runs when old space fills
- Mark-Compact: Like Mark-Sweep but also defragments memory
Monitoring V8 GC
Built-in GC Traces:
# Enable GC logging
node --trace-gc server.js
# Output:
# [12345:0x104800000] 42 ms: Scavenge 8.5 (10.2) -> 7.8 (10.2) MB
# [12345:0x104800000] 123 ms: Mark-sweep 95.2 (100.5) -> 82.3 (100.5) MB
Parse GC logs:
[PID:address] time ms: GC-type before (heap) -> after (heap) MB
Scavenge = Minor GC (young generation)
Mark-sweep = Major GC (old generation)
Prometheus Metrics:
import { Gauge } from 'prom-client';
import v8 from 'v8';
const heap_size = new Gauge({ name: 'nodejs_heap_size_total_bytes' });
const heap_used = new Gauge({ name: 'nodejs_heap_used_bytes' });
const gc_duration = new Histogram({
name: 'nodejs_gc_duration_seconds',
labelNames: ['kind']
});
// Track GC events
const PerformanceObserver = require('perf_hooks').PerformanceObserver;
const obs = new PerformanceObserver((list) => {
const entry = list.getEntries()[0];
gc_duration.labels(entry.kind).observe(entry.duration / 1000);
});
obs.observe({ entryTypes: ['gc'] });
// Update heap metrics every 10s
setInterval(() => {
const stats = v8.getHeapStatistics();
heap_size.set(stats.total_heap_size);
heap_used.set(stats.used_heap_size);
}, 10000);
V8 GC Tuning
Heap Size Limits:
# Default: ~1.4GB on 64-bit systems
# Increase max heap size
node --max-old-space-size=4096 server.js # 4GB heap
# For containers (set to 75% of container memory)
# 8GB container → --max-old-space-size=6144
GC Optimization Flags:
# Aggressive GC (lower memory, more CPU)
node --optimize-for-size --gc-interval=100 server.js
# Optimize for throughput (higher memory, less CPU)
node --max-old-space-size=8192 server.js
# Expose GC to JavaScript
node --expose-gc server.js
# Then: global.gc() to force GC
When to tune:
- ✅ Container memory limits (set heap to 75% of limit)
- ✅ Frequent Major GC causing latency spikes
- ✅ OOM errors with available memory
- ❌ Don't tune as first step (fix leaks first!)
Python Garbage Collector
GC Mechanism
Two Systems:
- Reference Counting: Primary mechanism, immediate cleanup when refcount = 0
- Generational GC: Handles circular references
Generational Structure:
┌─────────────────────────────────────────────────────────┐
│ Python GC (Generational) │
├─────────────────────────────────────────────────────────┤
│ Generation 0 (Young) - Threshold: 700 objects │
│ ├─ New objects │
│ └─ Collected most frequently │
│ │
│ Generation 1 (Middle) - Threshold: 10 collections │
│ ├─ Survived 1 Gen0 collection │
│ └─ Collected less frequently │
│ │
│ Generation 2 (Old) - Threshold: 10 collections │
│ ├─ Survived Gen1 collection │
│ └─ Collected rarely │
└─────────────────────────────────────────────────────────┘
Monitoring Python GC
GC Statistics:
import gc
# Get GC stats
print(gc.get_stats())
# [{'collections': 42, 'collected': 123, 'uncollectable': 0}, ...]
# Get object count by generation
print(gc.get_count())
# (45, 3, 1) = (gen0, gen1, gen2) object counts
# Get thresholds
print(gc.get_threshold())
# (700, 10, 10) = collect when gen0 has 700 objects, etc.
Track GC Pauses:
import gc
import time
class GCMonitor:
def __init__(self):
self.start_time = None
def on_gc_start(self, phase, info):
self.start_time = time.time()
def on_gc_finish(self, phase, info):
duration = time.time() - self.start_time
print(f"GC {phase}: {duration*1000:.1f}ms, collected {info['collected']}")
# Install callbacks
gc.callbacks.append(GCMonitor().on_gc_start)
Prometheus Metrics:
from prometheus_client import Gauge, Histogram
import gc
gc_collections = Gauge('python_gc_collections_total', 'GC collections', ['generation'])
gc_collected = Gauge('python_gc_objects_collected_total', 'Objects collected', ['generation'])
gc_duration = Histogram('python_gc_duration_seconds', 'GC duration', ['generation'])
def record_gc_metrics():
stats = gc.get_stats()
for gen, stat in enumerate(stats):
gc_collections.labels(generation=gen).set(stat['collections'])
gc_collected.labels(generation=gen).set(stat['collected'])
Python GC Tuning
Disable GC (for batch jobs):
import gc
# Disable automatic GC
gc.disable()
# Process large dataset without GC pauses
for chunk in large_dataset:
process(chunk)
# Manual GC at end
gc.collect()
Adjust Thresholds:
import gc
# Default: (700, 10, 10)
# More aggressive: collect more often, lower memory
gc.set_threshold(400, 5, 5)
# Less aggressive: collect less often, higher memory but faster
gc.set_threshold(1000, 15, 15)
Debug Circular References:
import gc
# Find objects that can't be collected
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
print(f"Uncollectable: {len(gc.garbage)}")
for obj in gc.garbage:
print(type(obj), obj)
When to tune:
- ✅ Batch jobs: disable GC, manual collect at end
- ✅ Real-time systems: adjust thresholds to avoid long pauses
- ✅ Debugging: use
DEBUG_SAVEALLto find leaks - ❌ Don't disable GC in long-running services (memory will grow!)
GC-Related Memory Issues
Issue 1: Long GC Pauses
Symptom: Request latency spikes every few minutes
V8 Fix:
# Monitor GC pauses
node --trace-gc server.js 2>&1 | grep "Mark-sweep"
# If Major GC >500ms, increase heap size
node --max-old-space-size=4096 server.js
Python Fix:
# Disable GC during request handling
import gc
gc.disable()
# Periodic manual GC (in background thread)
import threading
def periodic_gc():
while True:
time.sleep(60)
gc.collect()
threading.Thread(target=periodic_gc, daemon=True).start()
Issue 2: Frequent Minor GC
Symptom: High CPU from constant minor GC
Cause: Too many short-lived objects
Fix: Reduce allocations
# ❌ BAD: Creates many temporary objects
def process_data(items):
return [str(i) for i in items] # New list + strings
# ✅ BETTER: Generator (no intermediate list)
def process_data(items):
return (str(i) for i in items)
Issue 3: Memory Not Released After GC
Symptom: Heap usage high even after GC
V8 Cause: Objects in old generation (major GC needed)
# Force full GC to reclaim memory
node --expose-gc server.js
# In code:
if (global.gc) global.gc();
Python Cause: Reference cycles
# Debug reference cycles
import gc
import sys
# Find what's keeping object alive
obj = my_object
print(sys.getrefcount(obj)) # Should be low
# Get referrers
print(gc.get_referrers(obj))
GC Alerts (Prometheus)
# Prometheus alert rules
groups:
- name: gc_alerts
rules:
# V8: Major GC taking too long
- alert: SlowMajorGC
expr: nodejs_gc_duration_seconds{kind="major"} > 0.5
for: 5m
annotations:
summary: "Major GC >500ms ({{ $value }}s)"
# V8: High GC frequency
- alert: FrequentGC
expr: rate(nodejs_gc_duration_seconds_count[5m]) > 10
for: 10m
annotations:
summary: "GC running >10x/min"
# Python: High Gen2 collections
- alert: FrequentFullGC
expr: rate(python_gc_collections_total{generation="2"}[1h]) > 1
for: 1h
annotations:
summary: "Full GC >1x/hour (potential leak)"
Best Practices
V8 (Node.js)
- Set heap size:
--max-old-space-sizeto 75% of container memory - Monitor GC: Track duration and frequency with Prometheus
- Alert on slow GC: Major GC >500ms indicates heap too small or memory leak
- Don't force GC: Let V8 manage (except for tests/debugging)
Python
- Use reference counting: Most cleanup is automatic (refcount = 0)
- Avoid circular refs: Use
weakreffor back-references - Batch jobs: Disable GC, manual
gc.collect()at end - Monitor Gen2: Frequent Gen2 collections = potential leak
Related Documentation
- Patterns: memory-optimization-patterns.md
- Tools: profiling-tools.md
- Examples: Examples Index
Return to reference index