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