Reference Counting Garbage Collection in JavaScript
This article provides a concise explanation of reference counting garbage collection in JavaScript, examines how circular references cause memory leaks by circumventing this mechanism, and explores how modern JavaScript runtimes solve this problem using reachability algorithms.
What Is Reference Counting Garbage Collection?
Reference counting is a memory management algorithm where the runtime tracks the total number of references pointing to each allocated object in memory.
The algorithm operates on a simple premise: 1. When an object is
created and assigned to a variable, its reference count is set to 1. 2.
Whenever another variable or property points to this object, the
reference count increments by 1. 3. When a reference to the object is
overwritten, set to null, or goes out of scope, the count
decrements by 1. 4. When the reference count reaches zero
(0), the object is deemed unreachable and its allocated
memory is immediately freed.
How Circular References Defeat Reference Counting
A circular reference occurs when two or more objects reference each other, creating a closed loop. In a pure reference-counting system, this loop prevents the reference counter from ever reaching zero, even if the objects are no longer accessible by the rest of the application.
Consider the following example:
function createCircularReference() {
const objectA = {};
const objectB = {};
// Create mutual references
objectA.other = objectB; // objectB count = 2 (local variable + objectA.other)
objectB.other = objectA; // objectA count = 2 (local variable + objectB.other)
return "done";
}
createCircularReference();When createCircularReference() finishes executing, both
objectA and objectB fall out of scope. In an
ideal scenario, their memory should be released.
However, under reference counting: - The local variable references
are dropped, decrementing each object’s count from 2 to 1. -
objectA still holds a reference to objectB,
keeping objectB’s count at 1. - objectB still
holds a reference to objectA, keeping
objectA’s count at 1.
Because neither count drops to zero, the garbage collector never reclaims the memory allocated for either object, resulting in a memory leak.
The Modern Solution: Mark-and-Sweep
Due to the limitation with circular references, modern JavaScript engines (such as V8, SpiderMonkey, and JavaScriptCore) no longer rely on simple reference counting. Instead, they use tracing garbage collection algorithms, predominantly Mark-and-Sweep.
Instead of asking whether an object has zero references, the Mark-and-Sweep algorithm asks whether an object is reachable from a set of root objects (such as the global object and currently active execution stack variables).
- Roots Phase: The collector starts at the roots.
- Mark Phase: It traverses all references outward from the roots and marks every object it can find as “reachable.”
- Sweep Phase: Any object in memory that was not marked during the traversal is unreachable from the root, meaning it cannot be used by the program. The engine safely reclaims its memory.
Under Mark-and-Sweep, isolated circular references are simply skipped during the traversal because there is no path from the root to the circular group. Consequently, the entire circular structure is collected and freed correctly.