completefrontend
Open lab My progress
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
    Follow the markingStart at the roots, follow each link, and say why a marked object is kept.
  • 02
    Spot fragmentationRead a free list and tell whether a big object can fit in it.
  • 03
    Explain compactionShow how a forwarding table keeps every reference correct after objects move.

Find the objects in use, free the rest

Your program makes objects all the time. Each object takes up some memory. When the program can no longer reach an object, its memory can be used again. The part of the JavaScript engine that frees this memory is the garbage collector, or collector for short.

This lesson shows two ways a collector can work:

  • Mark-sweep: find the objects still in use, then free the rest.
  • Mark-compact: do the same, then move the objects still in use close together.
The main idea

A collector works in steps. Each step is called a phase:

  • Marking: start at the roots and follow every link. Mark each object you reach.
  • Sweeping: free the memory of every object that was not marked.
  • Compaction: move the marked objects next to each other, so the free memory is in one piece.

A root is a value the program can always reach, such as a variable in the code that is running now. A reference is a link from one value to an object. For example, cart.items links the cart to its items.

This lesson follows Reference counting & cycles. There, the collector counted the links to each object. Two objects that linked to each other were hard to free. Marking asks a different question: can the program still reach this object, starting from a root?

Marking from roots

Marking starts at the roots. From each root, the collector follows every reference. It marks each object it reaches as live, which means still in use.

Follow a path from a rootPop out in the code editor (opens in a new tab)JavaScript
const root = { cart: { items: ["tea"] } };console.log(root.cart.items[0]);

Line 1 makes a root object. Inside it is a cart object, and inside that is an items array. Line 2 follows the path root.cart.items[0] and prints tea. Each step along that path follows a reference. The collector follows the same links that your code does.

The name root is not what makes it a root. A value is a root because the running program, or the browser or Node around it, still holds it.

The next code is a small model of marking. Each name stands for an object. Each list holds the names that object links to.

Mark the names the roots reachPop 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(","));

In plain words, line by line:

  1. Line 1 makes graph. cart links to items. user, items, and oldBanner link to nothing.
  2. Line 2 says the roots are cart and user.
  3. Line 3 marks the roots by putting them in marked. A Set is a list that keeps each value only once.
  4. Lines 4 to 6 go through each root and mark every name it links to. So items gets marked too.
  5. Line 7 prints the marked names, joined with commas: cart,user,items.

oldBanner is not printed. Nothing links to it, so the program can no longer reach it.

A roots tag with arrows to cart and user. An arrow goes from cart to items. Cart, user, and items are marked. oldBanner has no arrow coming in, so it is not reached.rootsstart herecartmarkedusermarkeditemsmarkedoldBannernot reached
Marking starts at the roots, cart and user, and follows each arrow. It reaches items through cart. No arrow reaches oldBanner.

This short loop only follows links one step away from the roots. Real objects can link many steps deep. The next model uses a worklist: a to-do list of names still to visit. It takes one name off the list, marks it, and adds that name's links to the list. It stops when the list is empty.

Mark with a to-do listPop 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(","));

Lines 1 to 6 write the same graph over several lines. Line 7 picks cart and user as roots. Line 8 calls markGraph, which the hidden setup code defines. It sorts the marked names before it returns them, so this prints cart,items,user.

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

These steps were recorded from the lesson's model code. They don't show the real engine.

Running in
  1. script
Next: line 1
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(","));
CallStoreChangeResultBlue line: runs next. 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

You clean your room in two rounds. First, you put a sticker on everything you still use. Then you throw away everything without a sticker.

When you put a sticker on a box, you also look inside it. The things inside get stickers too.

In real life: Put a sticker on things you still use
In JavaScript: Mark the objects the roots reach
In real life: Put stickers on the things inside a box
In JavaScript: Follow a reference to another object
In real life: Throw away things without a sticker
In JavaScript: Sweep: free the objects that are not marked

Sweeping and free lists

After marking, the collector sweeps. Memory is split into slots. In this lesson, one slot holds one object. Sweeping walks through the slots one by one. It keeps the marked objects and empties every other slot.

Keep only the 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 makes a heap: a list of slots. The heap is the part of memory where objects live. Line 2 marks cart and user. Line 3 goes through each slot with map. It keeps a marked name and puts null in place of any other name. It prints [ 'cart', null, 'user' ].

A free list is a list of the slot numbers that are now empty. When the program makes a new object later, the engine can put it in one of those slots.

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);

In plain words, line by line:

  1. Lines 1 to 5 make a function, sweep. It takes the heap and the marked names.
  2. Line 2 puts the marked names in a Set, so checking a name is quick.
  3. Line 3 keeps each marked name and puts null in every other slot.
  4. Line 4 returns the new heap and the free list: the numbers of the slots that hold null.
  5. Line 6 makes six slots. Slots 1 and 4 are already empty. Slot 3 holds old-banner, which was not marked.
  6. Line 7 sweeps, with cart, user, and items as the marked names.
  7. Lines 8 and 9 print the new heap and the free list, [ 1, 3, 4 ].
Six slots, numbered 0 to 5. Before the sweep: cart, null, user, old-banner, null, items. After the sweep: cart, null, user, null, null, items. The free list is 1, 3, 4.before the sweepslot 0slot 1slot 2slot 3slot 4slot 5cartnulluserold-bannernullitemsafter the sweepcartnullusernullnullitemsfree list: [ 1, 3, 4 ]
The sweep keeps the marked names where they are. It turns the unmarked old-banner into null, an empty slot. The empty slots, 1, 3, and 4, make the free list.

This array is a small model. A real engine keeps more facts about each object, such as its size. The main idea is the same: sweeping frees memory without moving any object that is still in use.

Fragmentation

Fragmentation means the free memory is split into small gaps between objects that are in use. There can be enough free slots in total, but not enough of them side by side. A big object needs its slots side by side.

Are three free slots side by side?Pop 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 the free slots: 1, 3, and 4. Line 2 says the new object needs three slots. Line 3 calls hasRun, which the hidden setup code defines. It checks whether three free slots sit in a row. It prints false, because slot 2 is not free.

A new object needs 3 slots in a row. The slots below are cart, null, user, null, null, items. The free slots 1, 3, and 4 are split by user in slot 2, so the new object does not fit.new: 3 slotsneeds 3 free slots in a row012345cartnullusernullnullitemsdoes not fit
There are three free slots: 1, 3, and 4. But user sits in slot 2 between them. The longest row of free slots is only 2, so a three-slot object does not fit.

So there are two different questions:

  • How many slots are free in total? Here, three.
  • What is the longest row of free slots? Here, only two: slots 3 and 4.

A big object needs the second answer to be large enough.

Try it below. The playground sweeps the same six slots as before. Press the button to stop using items, and watch the longest row of free slots.

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);
The heap after the sweep

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

Free list: 1, 3, 4

Longest row of free slots: 2

Try it yourself

Free slots: 1, 3, 4. The longest row of free slots is 2, so a three-slot object does not fit without moving anything. After compaction, the slots would be: cart, user, items, empty, empty, empty.

items is still marked, so slot 5 stays in use. This is a small model: JavaScript code can't see or change memory slots.
Real-life analogyA parking lot with scattered spaces

Some cars leave a parking lot. Now there are three empty spaces, but they are spread out. A bus needs three spaces side by side, so it cannot park.

The attendant moves the parked cars close together. Now the empty spaces form one long row, and the bus fits. The attendant also tells each driver where their car is now.

In real life: Empty spaces are spread out
In JavaScript: Free slots have objects between them
In real life: A bus needs three spaces side by side
In JavaScript: A big object needs free slots side by side
In real life: Move the parked cars close together
In JavaScript: Compaction: move the objects in use together
In real life: Tell each driver the car's new space
In JavaScript: Fix every reference to a moved object

Compaction: moving objects together

Compaction slides the objects in use toward one end of the heap. Then all the free slots sit together at the other end, in one long row.

Moving has a cost. For each object it moves, the collector must:

  • copy the object to its new slot,
  • write down where the object went,
  • fix every reference that still points at the old slot.
Compact a small 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);

In plain words, line by line:

  1. Line 1 makes a heap with empty slots between cart, user, and items.
  2. Line 2 calls compactWithSteps. It slides the objects to the front. It also writes each move into a forwarding table: a list of "old slot to new slot" pairs.
  3. Line 3 has two pointers. Here, a pointer is a slot number that says where an object is. cartItems says slot 4 and activeUser says slot 2. updatePointers looks up each one in the forwarding table.
  4. Line 4 prints the packed heap: [ 'cart', 'user', 'items', null, null, null ].
  5. Line 5 prints the new pointers: { cartItems: 2, activeUser: 1 }.
Before: cart, null, user, null, items, null. After: cart, user, items, null, null, null. Arrows show user moving from slot 2 to slot 1 and items moving from slot 4 to slot 2. The forwarding table says 0 to 0, 2 to 1, 4 to 2. The pointers become cartItems 2 and activeUser 1.012345beforecartnullusernullitemsnullaftercartuseritemsnullnullnullold → new0 → 02 → 14 → 2{ cartItems: 2, activeUser: 1 }
Compaction slides user from slot 2 to slot 1 and items from slot 4 to slot 2. The forwarding table on the right records each move, so the pointers can be updated.
Step through compaction
Step 0 of 6Ready
Your turn: follow the blue line

These steps were recorded from the lesson's compact-and-fix model. They don't show the browser's real memory.

Running in
  1. script
Next: line 1
const pointers = { cartItems: 4, activeUser: 2 };const moved = compact(heap);const updated = updatePointers(pointers, moved.forwarding);console.log(moved.heap);console.log(updated); 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]));}
CallStoreChangeResultBlue line: runs next. 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 is the JavaScript engine inside Chrome and Node. It does not compact the whole heap every time. The heap is split into blocks called pages, and V8 compacts only the pages with the most gaps. On the other pages, a sweep leaves enough usable space, so V8 skips the cost of moving.

Updating pointers after a move

A forwarding table pairs each old slot with its new slot. When an object moves, the collector uses this table to fix every reference to it.

Look up one moved 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 the object in slot 4 moved to slot 2. Line 2 is a pointer that still says slot 4. Line 3 looks up slot 4 in the table and prints 2.

References to one object can sit in many places:

  • variables in functions that are running now,
  • properties of other objects,
  • the engine's own lists.

The collector fixes all of them. The forwarding table is a small model of that work. The next code puts the whole move together.

Move the objects, then fix the pointersPop out in the code editor (opens in a new tab)JavaScript
const heap = ["cart", null, "user", null, "items", null];
const pointers = { cartItems: 4, activeUser: 2 };
const moved = compact(heap);
const updated = updatePointers(pointers, moved.forwarding);
console.log(moved.heap);
console.log(updated);

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]));
}

In plain words, line by line:

  1. Line 1 makes a heap with gaps.
  2. Line 2 makes two pointers: cartItems says slot 4 and activeUser says slot 2.
  3. Line 3 compacts the heap and builds the forwarding table.
  4. Line 4 fixes both pointers with the forwarding table.
  5. Lines 5 and 6 print the packed heap and the fixed pointers, { cartItems: 2, activeUser: 1 }.
  6. Lines 8 to 14 make the two functions, compact and updatePointers.

Your JavaScript code can never see a slot number or a memory address. So you never notice a move. Your variables keep working because the collector fixed every reference.

JavaScript lets you compare two objects with === and read their properties. It never tells you where an object sits in memory, so your code should never depend on that.

A small V8 trace

Node can print one line each time V8 collects garbage. This printout is called a trace. The code below makes many objects and then asks for a collection.

Ask Node for a collection (Node only)JavaScript
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);

In plain words, line by line:

  1. Line 1 makes an empty array, keep.
  2. Line 2 adds 4000 small objects to it.
  3. Line 3 asks V8 to collect garbage now. globalThis.gc only exists when Node starts with the --expose-gc flag. It is for tests, not for real apps.
  4. Line 4 prints app output 4000.

The lesson test runs this code with node --trace-gc --expose-gc. The --trace-gc flag tells V8 to print a line for each collection. The test checks two things:

  • V8 printed Mark-Compact or Mark-sweep, the name of the collector it used.
  • The program still printed app output 4000. The collection did not change the program's own result.

Practical use

Your code can't choose which collector runs, or when. What your code can do is stop holding on to objects it no longer needs.

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 makes a cache: a Map that stores values under keys. Line 2 stores a cart under the key "cart". Line 3 deletes that entry when the cart is done. Line 4 prints 0, the number of entries left.

Deleting does not start a collection right away. It removes one path from a root to the cart. At the next collection, marking won't reach the cart, so its memory can be freed.

In a real site, first find out what keeps a big object alive. Then change the code. Common causes are:

  • an event listener that is never removed,
  • a timer that keeps running,
  • a cache that keeps growing,
  • a global variable.

The browser's memory tools can show the path from a root to an object. This path is called a retaining path.

Put each action in its phase
  • Start from cart and user.
  • Follow the link from cart to items.
  • Write the empty slot numbers into a free list.
  • Put null in place of old-banner, which was not marked.
  • Slide items from slot 4 to slot 2.
  • Change a pointer from slot 4 to slot 2, using 4 -> 2.
Try it yourself
0 of 6 correct

Sort the six cards by the phase they belong to: mark, sweep, or compact.

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 both start the same way: they mark the objects in use. The difference is what comes next:

  • Mark-sweep frees the gaps and leaves every object where it is.
  • Mark-compact moves the objects together, so the gaps join into one big free area.

Neither one fixes a memory leak on its own. A memory leak is memory your program keeps but no longer needs. If a cache, listener, timer, or global variable still reaches an object, marking will correctly keep it. First remove that link. Then the next collection can free the object.

  • “Sweeping always leaves one big empty area.” It can leave many small gaps between objects.
  • “Compaction changes my variable values.” No. The collector moves objects and fixes every reference, so your variables still point at the same objects.
  • “Three free slots mean a three-slot object fits.” Only if the three slots are side by side.

The table compares the two collectors. Read each row as one collector, from left to right.

Mark-sweep and mark-compact side by side
CollectorWhat does it do?What does it cost?What does free memory look like?What happens to references?
Mark-sweepIt marks the objects in use, then lists the other slots as free.It is quick, because nothing moves.Free slots can be spread out, with objects between them.No object moves, so no reference needs fixing.
Mark-compactIt marks the objects in use, then slides them together.It takes longer, because objects are copied.All free slots sit together in one long row.Every reference to a moved object must be fixed.

Practice exercises

Exercise 1 · Warm-upPredict the marked names

Read or run the code. Type what it prints.

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

    Look at the "Sweep slots into a free list" code. Which slot numbers are free after the sweep?

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

      Exercise 3 · PracticeCheck a three-slot fit

      Slots 1, 3, and 4 are free. Can a three-slot object fit? Type yes or no.

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

        Exercise 4 · PracticeFollow a move

        The forwarding table says 4 -> 2. The items pointer said slot 4. Which slot does it say now?

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

          Exercise 5 · ChallengeApply it to a real app

          You build a shopping app. The collector compacts memory while it runs. What should you assume about your variables?

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

            Check your understanding

            Use the same steps for every question:

            1. Find the roots.
            2. Mark every name the roots reach.
            3. Sweep: the rest become free slots.
            4. Check whether the free slots sit side by side.

            Three free slots do not mean a three-slot object fits. Always check where the free slots are.

            For questions about moving, remember two facts. Compaction puts the free slots in one row. The forwarding table lets the collector fix every reference. Your code sees the same object before and after the move, because JavaScript never shows you where an object sits in memory.

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

              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 8Three slots are free. Why might a three-slot object still not fit?

              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 8After compaction, what keeps references correct?

              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 it worth the time to move objects with mark-compact?

              Choose an answer to see the explanation.

            Key takeaways

            • Marking starts at the roots and follows every reference. Each object it reaches is in use.
            • Sweeping frees every slot that was not marked. The free slot numbers form a free list.
            • Fragmentation means the free slots are split up by objects in use.
            • Compaction moves the objects in use together, so the free slots form one long row.
            • A forwarding table pairs old and new slots, so the collector can fix every reference to a moved object.

            Remember the one-liner.
            Mark finds what is in use, sweep frees the rest, and compact closes the gaps.

            Coming next: Generational collection & the Scavenger. It uses the fact that most objects are only needed for a short time.

            CompleteFrontend Clear concepts. Working examples.