cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Mark-sweep & mark-compact

Learn how JavaScript collectors mark live objects, sweep free slots, handle fragmentation, compact memory, and update moved references.

By the end, you can
  • 01
    Trace markingStart from roots and explain why a marked object survives.
  • 02
    Spot fragmentationRead a free list and tell whether one large object can fit.
  • 03
    Explain compactionShow how forwarding slots keep references correct after objects move.

Find live objects, free the rest

A collector needs a clear question: which objects can the running program still reach? Mark-sweep marks those live objects, then frees the unmarked ones. Mark-compact also groups live objects together after finding them.

Definition

Marking follows references from roots. Sweeping reclaims unmarked slots. Compaction moves live objects together to remove holes.

This follows Reference counting & cycles. Counting had trouble with cycles; tracing answers a different question: can a root reach this object?

Marking from roots

Start with roots such as a current variable or host-held reference. Follow each reference. Every reached object is marked live.

A root reaches one valuePop out in the code editor (opens in a new tab)JavaScript
const root = { cart: { items: ["tea"] } };console.log(root.cart.items[0]);

Line 1 creates a root object, then connects it to a cart and an items array. Line 2 reads the first item through that whole path. It prints tea.

Marking is the collector phase that starts at roots and records every object it can reach. A root is not special because of its name. It is special because the running program or host still holds it.

Mark a tiny object graphPop out in the code editor (opens in a new tab)JavaScript
const graph = { cart: ["items"], user: [], items: [], oldBanner: [] };const roots = ["cart", "user"];const marked = new Set(roots);for (const name of roots) {  for (const child of graph[name]) marked.add(child);}console.log([...marked].join(","));

Line 1 creates four names and one reference from cart to items. Line 2 names the roots. Line 3 marks them. Lines 4 to 6 follow their children. Line 7 prints cart,user,items; oldBanner is unreachable.

A worklist marking modelPop out in the code editor (opens in a new tab)JavaScript
const graph = {  cart: ["items"],  user: [],  items: [],  oldBanner: [],};const roots = ["cart", "user"];console.log(markGraph(graph, roots).join(","));

Line 1 writes a graph where each name has a list of references. Line 7 chooses cart and user as roots. Line 8 calls the lesson model, which repeatedly takes one name from a worklist and adds its children. It also prints cart,items,user.

Step through marking
Step 0 of 6Ready
Your turn: follow the blue line

Replay of instrumented teaching code, not an engine debugger.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const roots = ["cart", "user"];const marked = new Set(roots);for (const name of roots) {  for (const child of graph[name]) marked.add(child);}console.log([...marked].join(","));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Real-life analogyCleaning your room

Put a sticker on everything you still use. Then throw away everything without a sticker. The sticker pass is marking, and the clearing pass is sweeping.

A sticker on one box can lead you to another box inside it. In the same way, marking follows a reference from a live object to another live object.

In real life: Put a sticker on things you still use
In JavaScript: Mark objects reachable from roots
In real life: Follow a sticker to the matching box
In JavaScript: Follow a reference to another object
In real life: Throw away things without a sticker
In JavaScript: Sweep unmarked objects

Where the analogy stops: A room cleaner knows what is useful to you. A collector only follows references.

Sweeping and free lists

A free list is a list of slots that sweeping made available. It lets later allocations reuse freed space.

Keep marked slotsPop out in the code editor (opens in a new tab)JavaScript
const heap = ["cart", "old-banner", "user"];const marked = new Set(["cart", "user"]);console.log(heap.map((name) => marked.has(name) ? name : null));

Line 1 has a live cart, an old banner, and a live user. Line 2 marks only the cart and user. Line 3 replaces the unmarked banner with null, so it prints an array holding cart, null, and user.

Sweeping walks the heap representation after marking. It keeps marked entries, clears unmarked entries, and remembers the cleared positions for a later allocation.

Sweep slots into a free listPop out in the code editor (opens in a new tab)JavaScript
function sweep(heap, marked) {  const live = new Set(marked);  const nextHeap = heap.map((name) => live.has(name) ? name : null);  return { heap: nextHeap, freeList: nextHeap.flatMap((name, slot) => name === null ? [slot] : []) };}const heap = ["cart", null, "user", "old-banner", null, "items"];const swept = sweep(heap, ["cart", "user", "items"]);console.log(swept.heap);console.log(swept.freeList);

Line 1 makes six labeled slots. Line 2 lists the live names from marking. Line 3 sweeps. Lines 4 and 5 print the new heap and free slot numbers. The model frees slots 1,3,4.

There is an important limit: this array is a teaching model. Real engines store headers, object sizes, and allocator metadata. The useful idea remains the same: sweep can reuse dead space without moving live objects.

Fragmentation

Fragmentation means free space is split into small holes. The total can be large enough, while no single adjacent run is large enough.

Ask whether three slots are adjacentPop out in the code editor (opens in a new tab)JavaScript
const freeSlots = [1, 3, 4];const needs = 3;console.log(hasRun(freeSlots, needs));

Line 1 lists three free positions. Line 2 says the new object needs three slots. Line 3 asks the lesson helper whether positions 1, 2, and 3 all exist. It prints false, because slot 2 is occupied.

Total free space answers “how much space exists?” The largest free run answers “what is the biggest thing that can fit right now?” Those are different questions.

Playground: make room for a three-slot object
Sweep a small heapJavaScript
function sweep(heap, marked) {
  const live = new Set(marked);
  const nextHeap = heap.map((name) => live.has(name) ? name : null);
  return { heap: nextHeap, freeList: nextHeap.flatMap((name, slot) => name === null ? [slot] : []) };
}
const heap = ["cart", null, "user", "old-banner", null, "items"];
const swept = sweep(heap, ["cart", "user", "items"]);
console.log(swept.heap);
console.log(swept.freeList);
Heap result

[cart, empty, user, empty, empty, items]

Free list: 1, 3, 4

Try it yourself

Free slots: 1, 3, 4. A three-slot object does not fit before compaction. After compaction, cart, user, items, empty, empty, empty.

This is a small teaching model. JavaScript code cannot read or change engine heap slots.
Real-life analogyA parking lot with scattered spaces

After cars leave, three free spaces may be scattered. A bus needing three spaces in a row cannot park. Moving parked cars together creates one long free area.

The lot has enough empty spaces in total, but not the run the bus needs. This is why a free list alone does not remove fragmentation.

In real life: Empty spaces are spread out
In JavaScript: Free slots are fragmented
In real life: A bus needs three neighboring spaces
In JavaScript: A large object needs an adjacent run
In real life: Move cars together
In JavaScript: Compact live objects
In real life: Update each parking slip
In JavaScript: Update references after moving

Where the analogy stops: Drivers can read and update paper slips. JavaScript cannot see its internal addresses.

Compaction and moving objects

Compaction slides live objects toward one end of the heap. The reward is one long free region. The cost is copying live objects and fixing every reference that used an old location.

Compact a tiny heapPop out in the code editor (opens in a new tab)JavaScript
const heap = ["cart", null, "user", null, "items", null];const moved = compactWithSteps(heap);const pointers = updatePointers({ cartItems: 4, activeUser: 2 }, moved.forwarding);console.log(moved.heap);console.log(pointers);

Line 1 starts with holes between three live names. Line 2 calls the compacting model. Line 3 translates two model pointers with the forwarding table. Lines 4 and 5 print the packed heap and the translated pointers.

Moving a live object is not free. The collector must copy its data, record where it went, and update references before the program observes the heap again.

Step through compaction
Step 0 of 6Ready
Your turn: follow the blue line

Replay of the lesson's compact-and-update model, not browser heap inspection.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  const live = heap.flatMap((name, oldSlot) => name === null ? [] : [{ name, oldSlot }]);  return { heap: [...live.map(({ name }) => name), ...Array(heap.length - live.length).fill(null)], forwarding: Object.fromEntries(live.map(({ oldSlot }, newSlot) => [oldSlot, newSlot])) };}function updatePointers(pointers, forwarding) {  return Object.fromEntries(Object.entries(pointers).map(([name, slot]) => [name, forwarding[slot] ?? slot]));}const moved = compact(["cart", null, "user", null, "items", null]);const updated = updatePointers({ cartItems: 4, activeUser: 2 }, moved.forwarding);console.log(moved.heap);console.log(updated);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

V8 compacts only the pages that are most fragmented. That is one simple way to avoid paying movement cost when ordinary sweeping leaves usable space.

Updating pointers after a move

A forwarding table records an old slot and its new slot. The collector uses it to update references while moving objects.

Translate one model pointerPop out in the code editor (opens in a new tab)JavaScript
const forwarding = { 4: 2 };const pointer = 4;console.log(forwarding[pointer]);

Line 1 says that old slot 4 moved to new slot 2. Line 2 holds a pointer to the old slot. Line 3 looks up the new slot and prints 2.

A collector must update references held in stack frames, heap objects, and runtime structures. The forwarding table is a short model of that bookkeeping.

Move slots and update model pointersPop out in the code editor (opens in a new tab)JavaScript
function compact(heap) {
  const live = heap.flatMap((name, oldSlot) => name === null ? [] : [{ name, oldSlot }]);
  return { heap: [...live.map(({ name }) => name), ...Array(heap.length - live.length).fill(null)], forwarding: Object.fromEntries(live.map(({ oldSlot }, newSlot) => [oldSlot, newSlot])) };
}
function updatePointers(pointers, forwarding) {
  return Object.fromEntries(Object.entries(pointers).map(([name, slot]) => [name, forwarding[slot] ?? slot]));
}
const moved = compact(["cart", null, "user", null, "items", null]);
const updated = updatePointers({ cartItems: 4, activeUser: 2 }, moved.forwarding);
console.log(moved.heap);
console.log(updated);

Line 1 makes holes. Line 2 models two references by slot number. Line 3 compacts the heap. Line 4 updates references. Lines 5 and 6 print the compact heap and correct new slots. JavaScript code can never see addresses, so this move is invisible: your variables still work because the collector updated every reference.

This is also why application code should not try to reason from an object address. JavaScript gives you object identity and properties, not an internal slot number.

A small V8 trace

Node can expose a test-only collection hook. This Node-only probe keeps application output separate from V8 trace text.

Node-only trace probeJavaScript
const keep = [];
for (let index = 0; index < 4000; index += 1) keep.push({ index, text: "tea".repeat(20) });
globalThis.gc();
console.log("app output", keep.length);

The test runs Node with --trace-gc --expose-gc. It checks for Mark-Compact or Mark-sweep, then confirms the program still prints app output 4000.

Practical use

You cannot choose a collector from application code. You can avoid retaining data accidentally and use memory tools when a real problem appears.

Remove a finished cart from a cachePop out in the code editor (opens in a new tab)JavaScript
const cache = new Map();cache.set("cart", { items: ["tea"] });cache.delete("cart");console.log(cache.size);

Line 1 creates a cache. Line 2 stores a cart. Line 3 removes that entry when it is no longer needed. Line 4 prints 0. The deletion does not force collection, but it removes one path that could keep the cart reachable.

In a real site, investigate a retaining path before changing code. A long-lived listener, timer, cache, or global can be the path that keeps a large object alive.

Put each action in the collector phase
  • Start from cart and user.
  • Follow cart to items.
  • Write empty slot numbers into a free list.
  • Replace unmarked old-banner with null.
  • Slide items from slot 4 to slot 2.
  • Change a pointer using 4 -> 2.
Try it yourself
0 of 6 correct

Sort the six cards by the job they describe.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

Mark-sweep and mark-compact are not competing answers to the marking question. Both first need to know which objects are live. The difference comes after that decision: sweeping reuses holes where they are, while compaction pays to move survivors and joins the holes into one larger area.

Neither phase makes a logical memory leak disappear by magic. If a cache, listener, timer, or global still reaches an object, marking will correctly keep it. First remove the accidental retaining path; then the next collection can treat the object as unmarked.

  • “Sweep always gives one big empty area.” It can leave scattered holes.
  • “Compaction changes my variable values.” The collector updates internal references.
  • “A free list proves a large object fits.” The slots must be adjacent.
Mark-sweep and mark-compact
CollectorWorkCostFragmentationPointers
Mark-sweepFind live objects, then add dead slots to a free list.Fast to reclaimCan leave scattered holesNo live object moves
Mark-compactFind live objects, then slide them together.Costs time to move objectsLeaves one long free areaAll moved references must update

Practice exercises

Exercise 1 · Warm-upPredict marked names

Type the printed names.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const graph = { cart: ["items"], user: [], items: [], oldBanner: [] };
const roots = ["cart", "user"];
const marked = new Set(roots);
for (const name of roots) {
  for (const child of graph[name]) marked.add(child);
}
console.log([...marked].join(","));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead a free list

    Which free slots did sweeping make available?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCheck a three-slot fit

      Can a three-slot object fit in free slots 1, 3, and 4?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFollow a move

        After forwarding 4 -> 2, what is the new items slot?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeApply it to a real app

          What should a shopping app developer assume when the collector compacts memory?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            Use the same order for every question: find the roots, mark the reachable names, sweep what remains, and then ask whether the free slots form one run. Do not jump from “three empty slots” to “a three-slot object fits” without checking their positions.

            For moving questions, keep two facts together. Compaction improves the shape of free space, and forwarding information keeps every internal reference correct. JavaScript sees the same object before and after the move because its address was never part of the language contract.

            Mark-sweep and compact quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What does marking decide?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const marked = new Set(["cart", "user"]);
              marked.add("items");
              console.log([...marked].join(","));

              Choose an answer to see the explanation.

            3. Question 3 of 8What is a free list?

              Choose an answer to see the explanation.

            4. Question 4 of 8Why can three free slots still reject a three-slot object?

              Choose an answer to see the explanation.

            5. Question 5 of 8What does compaction do?

              Choose an answer to see the explanation.

            6. Question 6 of 8What keeps references correct after compaction?

              Choose an answer to see the explanation.

            7. Question 7 of 8What does this fragmentation check print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const freeSlots = [1, 3, 4];
              console.log(freeSlots.includes(2) ? "fits" : "does not fit");

              Choose an answer to see the explanation.

            8. Question 8 of 8When is mark-compact worth its moving cost?

              Choose an answer to see the explanation.

            Key takeaways

            • Marking follows references from roots to find live objects.
            • Sweeping frees unmarked slots and can build a free list.
            • Fragmentation scatters free space into holes.
            • Compaction makes one long free area by moving live objects.
            • Forwarding information lets collectors update every moved reference.

            Remember the one-liner.
            Mark finds what lives; sweep frees what does not; compact removes the holes.

            Coming next: Generational collection & the Scavenger.

            CompleteFrontend Clear concepts. Working examples.