DEV Community

Cover image for The Life and Death of Variables: Memory Management in JS
Dineshraj Anandan
Dineshraj Anandan

Posted on • Originally published at dineshraj.hashnode.dev

The Life and Death of Variables: Memory Management in JS

Ever wondered what happens to all those objects you create in JavaScript? Unlike low-level languages like C, where developers manually allocate and free memory using malloc() and free(), JavaScript handles memory management automatically. But how does it work under the hood? Let's explore the fascinating world of garbage collection.

The Automatic Memory Janitor

JavaScript's garbage collector is like an invisible janitor working in the background, constantly monitoring your application's memory and cleaning up what's no longer needed. The engine automatically tracks objects through multiple phases, checking if they're still referenced by your program and releasing their memory when they're not.

But here's the catch: determining whether memory "is not needed anymore" is fundamentally an undecidable problem in computer science. So how do JavaScript engines tackle this challenge?

The Core Concept: Reachability

Modern garbage collection relies on one elegant principle: reachability. In the context of memory management, an object is considered reachable if your code can access it, either directly or through a chain of references.

Think of it like this: if you can't get to an object by following a path from your code's "roots" (like global variables or the current call stack), then it's effectively dead weight that can be cleaned up.

Mark-and-Sweep: The Foundation

All modern JavaScript engines use a variation of the Mark-and-Sweep algorithm. This clever approach reduces the complex question of "Is this object needed?" to the simpler "Is this object reachable?"

Mark-n-Sweep

Here's how it works in two distinct phases:

Phase 1: Marking

The garbage collector starts from root objects (global variables, objects on the call stack) and traverses the entire object graph, marking every reachable object as "in-use." It's like tracing a web of connections, following every link from the roots.

Phase 2: Sweeping

After marking, the collector scans the memory heap and reclaims memory from all unmarked objects. If it wasn't marked, it's unreachable and therefore safe to collect.

While this algorithm has remained fundamentally unchanged, modern improvements in the field (generational, incremental, concurrent, and parallel garbage collection) have focused on optimizing how it's implemented, not the core algorithm itself.

Generational Garbage Collection: Working Smarter

Here's an interesting observation: most objects in JavaScript are short-lived. They're created, used briefly, and then abandoned. JavaScript engines leverage this pattern through generational garbage collection, dividing the memory heap into generations based on object lifespan.

Young Generation (The Nursery)

Newly created objects start here. Since most objects die young, this area is collected frequently using a fast process called scavenging. Think of it as a quick sweep that happens often, efficiently reclaiming memory from short-lived objects without scanning the entire heap.

Scavenging is a highly efficient, stop-the-world algorithm that quickly identifies live objects and copies them from one memory space to another, leaving dead objects behind to be reclaimed.

Old Generation (The Tenured Space)

Objects that survive multiple garbage collection cycles in the young generation get "promoted" to the old generation. These long-lived objects are collected less frequently using full Mark-and-Sweep, which is more computationally intensive but happens much less often.

This two-tier approach dramatically improves performance by focusing resources where they're most needed.

The Limitation: No Manual Control

Unlike lower-level languages, JavaScript doesn't allow manual garbage collection control and this is by design. You cannot programmatically trigger garbage collection in core JavaScript (nor will you likely ever be able to). To release an object's memory, you must make it explicitly unreachable by removing all references to it.

However, there are ways to peek under the hood...

Tracing Garbage Collection in Node.js

Want to see garbage collection in action? Node.js provides flags that let you trace GC events:

Let's create a test script that deliberately stresses the garbage collector to observe both types of GC in action:

const leaks = [];

function generateGarbage() {
  const largeArray = [];
  for (let i = 0; i < 100; i++) {
    largeArray.push(new Array(100).join('.'));
  }
}

function retainMemory() {
  const persistentArray = new Array(100);
  for (let i = 0; i < 100; i++) {
    persistentArray[i] = new Array(100).join('.');
  }
  leaks.push(persistentArray);
  if (leaks.length > 5) {
    leaks.shift(); // Release old memory for collection
  }
}

// Continuously allocate and retain memory
let counter = 0;
const interval = setInterval(() => {
  generateGarbage();
  retainMemory();
  console.log('Heap status:', process.memoryUsage());
  if(counter > 100){
    clearInterval(interval);
  }
  counter++;
}, 100);
Enter fullscreen mode Exit fullscreen mode

What This Code Does

This script is specifically designed to trigger different types of garbage collection:

generateGarbage() creates short-lived objects that immediately become unreachable after the function returns. These objects are perfect candidates for young generation collection (scavenging). Since largeArray is a local variable with no external references, it becomes garbage as soon as the function exits.

retainMemory() creates long-lived objects by storing them in the global leaks array. These objects remain reachable and will survive multiple GC cycles, eventually getting promoted to the old generation. The function maintains a rolling window of 5 arrays, so older ones are eventually released by calling leaks.shift(), making them eligible for collection.

The interval loop runs every 100ms, continuously creating both types of memory pressure. This rapid allocation forces frequent garbage collection cycles, making GC activity visible in the trace output. Running it 100+ times ensures you'll see both minor (scavenge) and major (mark-sweep) collections.

⚠️ Warning: Execute the code with caution

Why This Script Is Perfect for Tracing

This code is ideal for observing GC behavior because it:

  1. Creates predictable memory patterns - You can clearly see the difference between short-lived garbage and retained objects

  2. Generates enough memory pressure - Rapid allocation triggers frequent GC events that show up in traces

  3. Demonstrates generational behavior - Objects that survive (leaks array) will be promoted to old generation, while temporary objects are quickly scavenged

  4. Shows the full lifecycle - Both allocation and deallocation (via shift()) are visible

Run this with tracing enabled:

node --trace-gc your-script.js
Enter fullscreen mode Exit fullscreen mode

This outputs GC events showing:

  • Type of GC (Scavenge for young generation, Mark-sweep for old generation)

  • Memory before and after collection (showing how much was reclaimed)

  • Time spent in GC (revealing performance impact)

You will see output like:

[16215:0x150008000]     8795 ms: Scavenge 5.6 (8.3) -> 4.1 (8.3) MB, 0.17 / 0.00 ms  (average mu = 1.000, current mu = 1.000) task; 
[16215:0x150008000]     9023 ms: Mark-Compact (reduce) 4.9 (8.3) -> 3.0 (5.3) MB, 1.46 / 0.00 ms  (+ 1.3 ms in 7 steps since start of marking, biggest step 0.3 ms, walltime since start of marking 3 ms) (average mu = 1.000, current mu = 1.000) finalize incremental marking via task; GC in old space requested
Enter fullscreen mode Exit fullscreen mode

For more details, use:

node --trace-gc --trace-gc-verbose your-script.js
Enter fullscreen mode Exit fullscreen mode

Manual Triggering (Not Recommended for Production)

You can manually trigger GC using the --expose-gc flag, but this is strictly for debugging purposes:

node --expose-gc your-script.js
Enter fullscreen mode Exit fullscreen mode
if (global.gc) {
  global.gc(); // Manually trigger garbage collection
}
Enter fullscreen mode Exit fullscreen mode

Important: Never use this in production environments. The garbage collector is optimized to run at the right times. Manual intervention usually does more harm than good.

Tracing GC and triggering GC can also be done on frontend apps using Browser DevTools

The Bottom Line

JavaScript's garbage collection is a sophisticated system that operates silently, allowing developers to focus on building features rather than managing memory. While you can't control it directly, understanding how it works helps you write more memory-efficient code and debug potential memory leaks.

The key takeaway? Make objects unreachable when you're done with them, and let the garbage collector do its job. It's been optimized for decades. Trust the process.

Top comments (0)