V8 Memory Structure
Node.js uses the V8 JavaScript engine which manages memory automatically through garbage collection. Understanding how memory is organized helps you write more efficient applications and debug memory issues.
π§ V8 Memory Layout
Heap
Where objects, strings, and closures are stored. This is where memory leaks occur.
Stack
Function call frames and primitive values. Automatically cleaned up.
External Memory
Buffers and C++ objects. Not counted in heap statistics.
Checking Memory Usage
// Get memory usage
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)} MB`, // Total memory
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`, // V8 heap allocated
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`, // V8 heap used
external: `${Math.round(used.external / 1024 / 1024)} MB` // C++ objects
});
// Monitor memory over time
setInterval(() => {
const { heapUsed } = process.memoryUsage();
const mb = Math.round(heapUsed / 1024 / 1024);
console.log(`Heap: ${mb} MB`);
}, 5000);
// V8 heap statistics
const v8 = require('v8');
const heapStats = v8.getHeapStatistics();
console.log({
heapSizeLimit: `${Math.round(heapStats.heap_size_limit / 1024 / 1024)} MB`,
totalAvailable: `${Math.round(heapStats.total_available_size / 1024 / 1024)} MB`
});
Common Memory Leaks
// 1. Global variables (intentional or accidental)
// BAD: Accidental global
function leak() {
data = []; // Missing 'let' or 'const'!
for (let i = 0; i < 10000; i++) {
data.push(new Array(1000));
}
}
// 2. Closures holding references
// BAD: Closure keeps entire array in memory
function createHandler() {
const bigData = new Array(1000000).fill('x');
return function handler() {
// Even if we only use bigData.length,
// the entire array stays in memory
console.log(bigData.length);
};
}
// FIXED: Only keep what you need
function createHandlerFixed() {
const bigData = new Array(1000000).fill('x');
const length = bigData.length; // Extract needed value
return function handler() {
console.log(length);
};
}
// 3. Event listeners not removed
// BAD: Adding listeners without cleanup
class LeakyComponent {
constructor() {
window.addEventListener('resize', this.onResize);
}
onResize = () => { /* ... */ };
// Never removes the listener!
}
// FIXED: Clean up listeners
class FixedComponent {
constructor() {
this.boundResize = this.onResize.bind(this);
window.addEventListener('resize', this.boundResize);
}
onResize() { /* ... */ }
destroy() {
window.removeEventListener('resize', this.boundResize);
}
}
// 4. Growing arrays/maps
// BAD: Cache grows forever
const cache = new Map();
function getCached(key) {
if (!cache.has(key)) {
cache.set(key, expensiveComputation(key));
}
return cache.get(key);
}
// FIXED: Use LRU cache with size limit
const LRU = require('lru-cache');
const cache = new LRU({ max: 500 });
Garbage Collection in V8
V8 uses generational garbage collection with two main spaces:
| Space | Description | Collection |
|---|---|---|
| New Space (Young Gen) | Newly allocated objects | Minor GC (Scavenge) - Fast |
| Old Space (Old Gen) | Objects that survived 2 GCs | Major GC (Mark-Sweep) - Slow |
# Run with GC tracing
node --trace-gc app.js
# Expose GC for manual triggering (debugging only!)
node --expose-gc app.js
// In code (only when --expose-gc is set)
if (global.gc) {
global.gc();
}
Heap Snapshots
const v8 = require('v8');
const fs = require('fs');
// Take a heap snapshot
function takeHeapSnapshot() {
const snapshotStream = v8.writeHeapSnapshot();
console.log('Heap snapshot written to:', snapshotStream);
}
// Programmatic heap dump
const heapSnapshot = v8.getHeapSnapshot();
const filename = `heap-${Date.now()}.heapsnapshot`;
const fileStream = fs.createWriteStream(filename);
heapSnapshot.pipe(fileStream);
// Using inspector module for detailed analysis
const inspector = require('inspector');
const session = new inspector.Session();
session.connect();
session.post('HeapProfiler.takeHeapSnapshot', null, (err, r) => {
console.log('Snapshot taken');
session.disconnect();
});
// Load snapshot in Chrome DevTools:
// 1. Open chrome://inspect
// 2. Click "Open dedicated DevTools for Node"
// 3. Go to Memory tab
// 4. Load the .heapsnapshot file
Memory Profiling Tools
# 1. Node.js built-in inspector
node --inspect app.js
# Then open Chrome DevTools β Memory tab
# 2. Clinic.js (recommended)
npm install -g clinic
clinic doctor -- node app.js
clinic heapprofiler -- node app.js
# 3. Node with increased memory limit
node --max-old-space-size=4096 app.js # 4GB heap
# 4. Get heap usage report
node --heap-prof app.js
# Creates .heapprofile file for Chrome DevTools
Memory Leak Detection
// Simple leak detector
class LeakDetector {
constructor() {
this.samples = [];
this.interval = null;
}
start(sampleInterval = 5000) {
this.interval = setInterval(() => {
const { heapUsed } = process.memoryUsage();
this.samples.push(heapUsed);
// Keep last 20 samples
if (this.samples.length > 20) {
this.samples.shift();
}
// Check for consistent growth
if (this.samples.length >= 10) {
const growing = this.samples.every((val, i) =>
i === 0 || val > this.samples[i - 1]
);
if (growing) {
console.warn('β οΈ Possible memory leak detected!');
console.log('Heap growth:', this.samples.map(s =>
Math.round(s / 1024 / 1024) + 'MB'
).join(' β '));
}
}
}, sampleInterval);
}
stop() {
clearInterval(this.interval);
}
}
// Usage
const detector = new LeakDetector();
detector.start();
// Using memwatch-next for automatic detection
const memwatch = require('@airbnb/node-memwatch');
memwatch.on('leak', (info) => {
console.error('Memory leak detected:', info);
});
memwatch.on('stats', (stats) => {
console.log('GC stats:', stats);
});
Best Practices for Memory Efficiency
// 1. Use streams for large data
// BAD
const data = fs.readFileSync('huge.json');
const parsed = JSON.parse(data);
// GOOD
const JSONStream = require('JSONStream');
fs.createReadStream('huge.json')
.pipe(JSONStream.parse('*'))
.on('data', item => processItem(item));
// 2. Nullify references when done
let bigObject = createBigObject();
processBigObject(bigObject);
bigObject = null; // Allow GC
// 3. Use WeakMap/WeakSet for caches
// Regular Map: holds strong reference
const cache = new Map();
cache.set(obj, data); // obj can't be GC'd
// WeakMap: allows GC when no other references
const weakCache = new WeakMap();
weakCache.set(obj, data); // obj can be GC'd
// 4. Avoid creating objects in hot paths
// BAD: Creates new object every call
function processItem(x) {
return { result: x * 2 };
}
// GOOD: Reuse object
const resultObj = { result: 0 };
function processItem(x) {
resultObj.result = x * 2;
return resultObj;
}
// 5. Use Buffer.allocUnsafe for performance
// Slower but zeroed
const safeBuf = Buffer.alloc(1024);
// Faster, may contain old data
const unsafeBuf = Buffer.allocUnsafe(1024);
π‘ Best Practices
- β’ Monitor memory usage in production with APM tools
- β’ Set memory limits:
--max-old-space-size=4096 - β’ Use WeakMap/WeakSet for caches that shouldn't prevent GC
- β’ Remove event listeners when components are destroyed
- β’ Take heap snapshots before and after operations to find leaks