Garbage Collection in JavaScript Explained Visually
#javascript
#gc
#visualization
#webdev
#tutorial
Introduction
JavaScript manages memory for you with garbage collection (GC). That means you don’t manually allocate and free memory like in some lower-level languages. Instead, the engine tracks which objects are still reachable from the roots (like global variables and the call stack) and periodically frees memory that is no longer reachable. This post explains the idea visually and shows how small coding choices can impact GC performance.
What is garbage collection?
- An object is considered live if it can be accessed from a root reference (global scope, the current call stack, or reachable objects).
- If an object becomes unreachable, the memory it occupies can be reclaimed.
- Garbage collection is typically automatic and happens during idle moments or when memory pressure is high.
Visualizing a tiny heap
Imagine a small heap with objects and references. Roots are the entry points the engine starts from.
- Root set: R (global variables, currently executing stack)
- Heap objects: A, B, C, D, E
A -> B B -> C D -> E C has no outgoing references E has no outgoing references
Visual diagram (simple ASCII):
Root set: R R -> A A -> B B -> C D -> E
Where reachable: R, A, B, C
Where unreachable (and eligible for GC): D, E
During a GC cycle, the engine marks all objects reachable from R (A, B, C, and R). Any object not marked (D and E) becomes a candidate for collection.
The two classic GC phases (mark and sweep)
Phase 1: Mark
- Start from roots and traverse all reachable objects.
- Mark each reached object as alive (e.g., with a flag).
Phase 2: Sweep
- Scan the heap and reclaim memory from objects that weren’t marked.
- Clear marks in preparation for the next cycle.
ASCII illustration:
Before marking: R -> A -> B -> C D -> E
Mark phase: R*, A*, B*, C* (the asterisk denotes marked)
Sweep phase: Collect D and E (free memory), keep A, B, C.
Common algorithms used in JavaScript engines
- Mark-and-sweep: The core idea described above, simple and robust.
- Generational GC: Most objects die young. The heap is divided into young and old generations. New objects start in the young generation; those that survive several cycles migrate to the old generation. This reduces pause times by focusing work on the young generation.
- Incremental and concurrent GC: Breaks GC work into smaller chunks to avoid long pauses, sometimes running alongside the main thread.
A step-by-step visual example
- Code creates objects:
let a = { name: “Alice” }; let b = { friend: a }; let c = { colleague: b };
-
Roots and references: R -> a -> b -> c
-
Some temporary object is created and then dereferenced: let temp = { data: [1,2,3] }; temp = null; // not reachable anymore
-
GC runs:
- Mark reachable: a, b, c, temp is not reachable after nulling
- Sweep: free temp
Tiny takeaway: Short-lived objects are good for GC because they quickly become unreachable. Long-lived, frequently referenced objects accumulate and can cause more frequent collection pressure.
Practical tips to reduce GC pauses
- Minimize allocations in hot paths: Reuse objects where possible instead of creating many temporary ones.
- Prefer object pools for frequently created objects if it makes sense for your app.
- Be careful with closures capturing large objects; they extend lifetimes unintentionally.
- Avoid creating large, complex graphs of objects in short bursts.
- Profile memory usage regularly to spot leaks early.
Tools to visualize and measure GC
- Chrome DevTools:
- Memory panel: take heap snapshots, inspect retained objects, and track allocations over time.
- Performance panel: record a session to see GC pauses and their timing.
- Node.js:
- Use —inspect with Chrome DevTools for heap snapshots and timeline analysis.
- Consider sampling profilers to understand allocation hotspots.
- Lightweight diagrams in debugging logs:
- Print object lifetimes and references in critical code paths to reason about reachability.
Summary
- JavaScript GC automatically frees memory by identifying live objects reachable from roots.
- The core idea is to mark reachable objects and sweep away the rest.
- Generational and incremental approaches help minimize pause times in real-world engines.
- You can influence GC performance by reducing allocations in hot paths and profiling memory usage to catch leaks early.
Further reading
- Understanding memory management concepts in JS engines
- Practical profiling workflows for front-end performance
- GC behavior differences across JavaScript engines (V8, SpiderMonkey, JavaScriptCore)