Lesson 5 of 8
5 min read
Advanced Node.js

Memory Management

Understand V8 heap, garbage collection, and how to prevent memory leaks

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

Continue Learning