completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Write barriers & remembered sets

Learn how JavaScript engines record old-to-young pointers so fast minor collections can keep young objects alive without scanning old memory.

By the end, you can
  • 01
    Spot the pointer that could be missedExplain why a pointer from an old object to a young object needs a note before a minor collection.
  • 02
    Follow the collector's notesTell remembered sets, store buffers, and card marking apart in small models you can run.
  • 03
    Explain the cost of a writeExplain why fast compiled code adds checks to writes, and when the engine can safely skip them.

The pointer a minor collection could miss

The garbage collector (GC) is the part of the JavaScript engine that frees memory your program no longer uses. Many engines split memory into two parts, called generations:

  • The young generation holds new objects. Most of them are needed only for a moment.
  • The old generation holds objects that have lived for a while.

A minor collection cleans only the young generation. It is fast because it does not look inside the old objects. Most new objects are already unused, so a short look at young memory is usually enough.

One more word: a pointer is how one object points at another object in memory. When you write cart[0] = item, the cart now holds a pointer to the item.

An old cart gets a new itemPop out in the code editor (opens in a new tab)JavaScript
const cart = []; // lived a long time, so imagine it is oldcart.push({ item: "tea" }); // new object, so imagine it is youngconsole.log(cart[0].item);

Line 1 makes an empty array called cart. The comment asks you to imagine it is old: it has been around for a long time. Line 2 makes a new object, { item: "tea" }, and puts it in the cart. The comment asks you to imagine this new object is young. Line 3 prints tea.

Left, in old memory: the cart, with slot 0 inside. Right, in young memory: the new object with item tea. An arrow goes from the cart to the tea object. Old memory is not scanned; young memory is scanned.old memory[0]cartnot scannedcart[0]young memory{ item: "tea" }scanned
The cart is old and the tea object is young. A quick young-only clean looks only at young memory, so it would not see the arrow that starts in the cart.

Line 2 creates a problem. A minor collection looks at young memory only. It does not look inside the old cart. Suppose nothing young points at the tea object. Then the collection could think tea is unused and free it, even though the cart still needs it.

The main idea

A write barrier is a small extra step that the engine adds to a write, such as cart[0] = tea. When an old object gets a pointer to a young object, the barrier writes down a note. A later minor collection reads the notes, so it can find that young object.

Real-life analogyYour phone's Downloads folder and Photos album

You clean your phone's Downloads folder, but you don't open the Photos album. One album entry links to a file in Downloads. If you delete that file, the album entry breaks. So each time you add such a link, you put a sticky note on Downloads: "the album uses this file". When you clean, you read the sticky notes first.

In real life: You clean Downloads quickly
In JavaScript: A minor collection cleans young memory
In real life: You don't open the Photos album
In JavaScript: The minor collection doesn't look inside old objects
In real life: An album entry links to a download
In JavaScript: An old object points at a young object
In real life: A sticky note names that link
In JavaScript: A note records the write

Adding the sticky note is the write barrier's job. In this lesson, the notes are small JavaScript models that you can run. They don't look at your browser's real memory. Real engines may store their notes in other ways.

Old-to-young pointers

An old-to-young pointer is a pointer stored in an old object that points at a young object. The direction matters:

  • Young to young: a minor collection already looks at young objects, so it finds this pointer.
  • Old to old: this pointer leads to no young object, so a minor collection doesn't need it.
  • Old to young: this is the one a minor collection could miss.
Name the direction of the pointerPop out in the code editor (opens in a new tab)JavaScript
const cart = { generation: "old" };const tea = { generation: "young" };console.log(cart.generation + "-to-" + tea.generation);

Line 1 makes a model cart with the label "old". Line 2 makes a model tea object with the label "young". Line 3 joins the two labels and prints old-to-young. These labels are only data for this lesson. Real engines don't give objects a generation property.

Here is the order of events:

  1. The program writes the pointer.
  2. Right after, the barrier checks the write: does it go from an old object to a young object?
  3. If yes, the barrier writes a note. The note is ready before the next minor collection starts.
Step through one write from an old cart to young tea
Step 0 of 7Ready
Your turn: follow the blue line

This replays small teaching code step by step. It is not a view into your browser's real notes.

Running in
  1. script
Next: line 1
const tea = { generation: "young" };const rememberedSet = []; function writeField(owner, key, value) {  owner[key] = value;  if (owner.generation === "old" && value.generation === "young") {    rememberedSet.push({ owner, key, value });  }} writeField(cart, "latest", tea);console.log(rememberedSet.length);
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.

The replay calls the lesson's real writeField function. Notice that the minor collection still won't look inside the old cart. The note is what leads it from the cart to tea.

Remembered sets

A remembered set is the collector's list of notes about old-to-young pointers. In this lesson's model, each note has three parts:

  • owner: the old object, like cart.
  • key: the property name, like "latest".
  • value: the young object it points at, like tea.
Write a note for one old-to-young writePop out in the code editor (opens in a new tab)JavaScript
const rememberedSet = []; function writeField(owner, key, value) {  owner[key] = value;  if (owner.generation === "old" && value.generation === "young") {    rememberedSet.push({ owner, key, value });  }} const cart = { generation: "old" };const tea = { generation: "young" };writeField(cart, "latest", tea);console.log(rememberedSet.length);

Line 1 starts rememberedSet as an empty array. Line 3 starts the writeField function, and line 4 stores the value in the owner. Line 5 checks two things: is the owner old, and is the value young? If both are true, line 6 adds a note. Line 12 writes tea into the old cart. Line 13 prints 1, because one note was added.

writeField(cart, latest, tea). The old cart now holds latest: tea. An arrow labeled barrier adds a note goes to a box of notes, which holds one card, cart.latest. The box is the rememberedSet.writeField(cart, "latest", tea)latest: teaold cartbarrieradds a notecart.latestnotesrememberedSet
Line 12 stores tea in the old cart. Because the owner is old and the value is young, the barrier also puts one note, cart.latest, in rememberedSet. So line 13 prints 1.

Real engines don't store notes as JavaScript objects like this. They may store memory addresses or other small, compact data. The goal is the same: a later minor collection can find every old-to-young pointer without looking at all of old memory.

The table lists four things a minor collection can use. You will meet store buffers and dirty cards later in this lesson. Read each row as: the thing, what it is, and why it helps.

Four things a minor collection can use
ThingWhat it isWhy it helps
Young rootA place the program can always reach, like a variable in use, that points at a young object.The minor collection starts from every young root, every time.
Remembered noteA note, written by the barrier, about one old-to-young pointer.The minor collection uses its young value as an extra starting point.
Store bufferA short list of recent writes that still have to be turned into notes.The engine turns its entries into notes later, so the write stays quick.
Dirty cardA flag that says a small part of old memory changed.The collection checks the slots in that part, not all of old memory.

Minor collection uses the notes

The notes help only if the collection reads them. A minor collection works in three steps:

  1. It starts from the young roots. A root is a place the program can always reach, like a variable in use. Young roots are roots that point at young objects.
  2. It adds the young values from the notes as extra starting points.
  3. It follows pointers from young object to young object, as usual. Every object it reaches is live (still in use). Any other young object can be freed.
A note keeps tea livePop out in the code editor (opens in a new tab)JavaScript
const youngRoots = [];const rememberedSlots = [{ owner: "cart", key: "latest", value: "tea" }]; function minorCollection(roots, slots) {  const live = new Set(roots);  for (const slot of slots) live.add(slot.value);  return [...live].sort();} console.log(minorCollection(youngRoots, rememberedSlots).join(","));

Line 1 says there are no young roots: the array is empty. Line 2 still has one note, from cart to tea. Line 4 starts the minorCollection function. Line 5 makes a set of live objects from the roots. Line 6 adds the value of each note. Line 10 prints tea. The note alone kept tea live in this model.

Box youngRoots is empty. Box slots holds one note, cart to tea. An arrow goes from the slots box to the tea card, which is marked live.youngRootsemptycart → teaslotsone notetealive
youngRoots is empty, so nothing young starts the search. The note in rememberedSlots still leads to tea, so tea stays live and the program prints tea.

This is the speed trade. The collection skips every old object, but the notes must be correct. Suppose one note were missing. Then a young object that is still in use could look unused, only because the one pointer to it starts in old memory. The collection would free it, and the program would break.

Step through a minor collection that reads a note
Step 0 of 5Ready
Your turn: follow the blue line

This replays a small model of a minor collection. It uses the notes as extra starting points. It does not clean your browser's memory.

Running in
  1. script
Next: line 1
const tea = { generation: "young" };const rememberedSet = [{ owner: cart, key: "latest", value: tea }]; function minorCollection(youngRoots, slots) {  const live = new Set(youngRoots);  for (const slot of slots) live.add(slot.value);  return [...live];} console.log(minorCollection([], rememberedSet).map((value) => value === tea ? "tea" : "?").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.

Remembered sets and store buffers

A store buffer is a short list of recent writes that matter to the collector. It splits the work into two jobs:

  • Now, during the write: add the write to the list. This is quick.
  • Later: go through the list and turn each entry into a note in the remembered set.

Different engines use different names for these parts, but the idea is common.

Add one write to the store bufferPop out in the code editor (opens in a new tab)JavaScript
const storeBuffer = []; function store(owner, key, value) {  owner[key] = value;  if (owner.generation === "old" && value.generation === "young") {    storeBuffer.push({ owner, key, value });  }} const cart = { generation: "old" };const tea = { generation: "young" };store(cart, "latest", tea);console.log(storeBuffer.length);

Line 1 makes an empty buffer. Line 3 starts the store function, and line 4 stores the value. Lines 5 and 6 add an entry only for an old-to-young write. Line 12 makes that write, so line 13 prints 1. Later, another part of the engine can go through this short list.

The buffer doesn't let the engine forget a write. It only changes when and where the rest of the work happens. The collection must read every entry before it decides which young objects are live.

Keep the names separate

A store buffer is usually the short list of recent writes. A remembered set is the set of notes that the minor collection reads. Engines can store them in different ways. Both exist so a minor collection can look at young memory only.

Card marking

Writing one exact note for every pointer can be too much work on every write. Card marking is a cheaper way:

  • Old memory is split into small, equal parts called cards. Each card holds a few slots. A slot is one place that holds a value, like one property.
  • When a write matters, the barrier marks its card as dirty. Dirty means "something changed here".
  • Later, the minor collection checks every slot in each dirty card.
Mark the card that holds slot 5Pop out in the code editor (opens in a new tab)JavaScript
const cardSize = 4;const cardTable = [false, false, false];const slot = 5;const card = Math.floor(slot / cardSize);cardTable[card] = true;console.log(cardTable.join(","));

Line 1 says each card holds four slots. Line 2 makes a card table: one true or false flag per card. All three start as false, which means clean. Line 3 picks slot 5. Line 4 divides 5 by 4 and rounds down with Math.floor, which gives card 1. Line 5 marks card 1 as dirty. Line 6 prints false,true,false. Only the middle card is dirty.

Twelve old slots, numbered 0 to 11, split into three cards of four. Slot 5 changed, so card 1 (slots 4 to 7) is dirty, which is true. Cards 0 and 2 stay clean, which is false.slot 5 changed0123card 0clean: false4567card 1dirty: true891011card 2clean: false
Every four slots share one card. Slot 5 is in card 1, so only card 1 is marked dirty. The program prints false,true,false.

A dirty card is less exact than a note for one slot, because it covers four slots. When the collection runs, some of those slots may not point at young objects any more. Checking them is extra work at collection time. In return, each write only has to set one flag, which is small and cheap.

Playground: mark the card for one slot
The four-slot card codePop out in the code editor (opens in a new tab)JavaScript
const cardSize = 4;const cardTable = [false, false, false];const slot = 5;const card = Math.floor(slot / cardSize);cardTable[card] = true;console.log(cardTable.join(","));
Card table0 dirty
card 0clean

Slots 0 to 3

card 1clean

Slots 4 to 7

card 2clean

Slots 8 to 11

Try it yourself

Slot 5 is in card 1, because 5 / 4 rounded down is 1. Dirty cards: none. A minor collection would check the slots in these cards only.

This is a small model. Real engines pick their own card sizes and store the flags in their own way.

The cost of barriers in JIT code

A JIT compiler (just-in-time compiler) turns JavaScript that runs often into fast machine code while the program runs. In that fast code, a property write is not always one simple step. The JIT may add a few checks to tell whether the write could create an old-to-young pointer. This small cost is normal for a collector with young and old memory.

A simple barrier checklistPop out in the code editor (opens in a new tab)JavaScript
function store(owner, value) {  if (valueIsSmallInteger(value)) return "no pointer";  if (owner.generation === "young") return "young owner";  if (value.generation === "old") return "old value";  recordOldToYoung(owner, value);  return "recorded";} console.log("recorded");

The checklist uses two made-up helper names, valueIsSmallInteger and recordOldToYoung. They stand for steps inside the engine. Line by line:

  1. Line 2 skips a small whole number. The engine stores it as the number itself, not as a pointer to an object.
  2. Line 3 skips when the owner is young, because the minor collection looks at young objects anyway.
  3. Line 4 skips an old value, because the write doesn't point into young memory.
  4. Line 5 records the only case left: an old owner and a young value. Line 6 returns "recorded".

The program never calls store. Line 9 only prints recorded, the name of the last step.

This is a sketch. It is not real machine code, and it is not exactly how any one engine works. Engines use what they learn while the code runs to make common safe writes cheap. For example, an engine may learn that a property always holds a number. When the engine can't be sure a write is safe, it keeps the barrier. Being correct comes first.

See three checklist resultsPop out in the code editor (opens in a new tab)JavaScript
function barrierDecision(ownerGeneration, value) {  if (typeof value === "number") return "skip: primitive";  if (ownerGeneration === "young") return "skip: young owner";  if (value.generation === "old") return "skip: old value";  return "record: old-to-young";} console.log(barrierDecision("old", 3));console.log(barrierDecision("young", { generation: "young" }));console.log(barrierDecision("old", { generation: "young" }));

A primitive is a simple value like a number or a string, not an object. Line 2 sees the number 3, a primitive, and skips. Line 3 skips a young owner. Line 4 would skip an old value. Only an old owner with a young object reaches line 5. So lines 8, 9, and 10 print skip: primitive, skip: young owner, and record: old-to-young.

So most writes are not expensive. The JIT must keep one rule: every old-to-young pointer gets a note. It also keeps common safe writes short. A few quick checks on each write usually cost much less than looking at all of old memory later.

What your app code should know

Your code never calls a writeBarrier() function after an assignment. JavaScript has no such function. The engine adds its own barrier steps to normal writes: property writes, array writes, and other writes that store an object. Your code stays normal JavaScript.

Normal code makes normal writesPop out in the code editor (opens in a new tab)JavaScript
const cart = [];cart.push({ item: "tea" });console.log(cart.length);

Line 1 makes an array. Line 2 puts a new object in it. Line 3 prints 1. If this array is old and the new object is young, the engine may run a barrier at line 2. Your program doesn't need to know how the engine stores its notes.

This helps when you read a profile: a recording, made with your browser's developer tools, of where your program spends its time. Many small writes can add some collector work. But changing code because of a guess rarely helps. First measure a real slowdown. Then use the browser's tools to find the cause.

Would the barrier write a note?
  • An old cart stores a young tea object.
  • An old cart stores the number 3.
  • A young cart stores a young tea object.
  • An old cart stores an old user object.
  • Slot 9 changes in old memory. Each card holds four slots.
  • An old list stores the price 120, a primitive.
Try it yourself
0 of 6 correct

Sort each write by the lesson's simple rule. After you place every card, read the explanations.

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

Common misconceptions

These words are easy to mix up, because they all happen inside the engine, below your JavaScript. Each one has one small job:

  • The write barrier notices a write.
  • The remembered set holds notes about old-to-young pointers.
  • Card marking marks a small part of old memory to check again.
  • “I should call a barrier from my app.” No. The engine runs it for you during normal writes.
  • “A remembered set copies all old objects.” No. It holds only notes about old-to-young pointers.
  • “A dirty card names one exact pointer.” No. It names a small part of memory that must be checked again.
  • “Minor collection can ignore old memory completely.” No. It doesn't look at all of old memory, but it must read the notes.
  • “Every write has the same cost.” No. JIT code can prove that many writes need no barrier work.

The table shows four terms. Each row says what the term is and what it is not.

Terms that sound alike but do different jobs
TermWhat it isWhat it is not
Write barrierA small step the engine adds to a write. It may write a note.It is not a JavaScript function you call yourself.
Remembered setA list of notes about old-to-young pointers.It is not a copy of all old objects.
Card markingMarking a small part of old memory as changed, so it gets checked again.It does not tell the collection the one exact slot that changed.
Minor collectionA clean of young memory that doesn't look at all of old memory.It cannot ignore old-to-young pointers. It must read the notes.

This lesson shows one common design. It is not a rule for every JavaScript engine. V8 (in Chrome), JavaScriptCore (in Safari), SpiderMonkey (in Firefox), and smaller engines can choose different details. Your JavaScript code behaves the same in all of them.

Practice exercises

Exercise 1 · Warm-upName the pointer

An old cart stores a new tea object. What kind of pointer does the barrier write a note about?

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

    Exercise 2 · Warm-upPredict the generation check

    Read the small model and type exactly what it prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cart = { generation: "old" };
    const tea = { generation: "young" };
    console.log(cart.generation === "old" && tea.generation === "young");

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

      Exercise 3 · PracticeKeep tea alive

      Only the old cart points at tea. What lets the model's minor collection keep tea?

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

        Exercise 4 · PracticeFind the card

        Each card holds four slots. Which card holds slot 9?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const cardSize = 4;
        const slot = 9;
        console.log(Math.floor(slot / cardSize));

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

          Exercise 5 · ChallengeSkip a safe write

          The program models an old object that stores the number 3. Why can the barrier skip writing a note?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const owner = { generation: "old" };
          const value = 3;
          console.log(typeof value === "number" ? "skip" : "record");

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

            Exercise 6 · ChallengeApply it to a real website

            After a change, a product grid on a website feels slow. The change makes many updates. You think barriers might be the cause. What should you do before you rewrite its assignments?

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

              Check your understanding

              For each question, first ask which way the pointer goes: old to young, or not. Then ask what the collector uses: an exact note, a store buffer entry, or a dirty card.

              Write barriers and remembered sets quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Why does a minor collection need notes about old objects?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const cart = { generation: "old" };
                const tea = { generation: "young" };
                console.log(cart.generation === "old" && tea.generation === "young");

                Choose an answer to see the explanation.

              3. Question 3 of 7In this lesson's model, what is a remembered set?

                Choose an answer to see the explanation.

              4. Question 4 of 7What is a store buffer?

                Choose an answer to see the explanation.

              5. Question 5 of 7Card marking makes each write cheaper. What does it cost later?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which write can fast JIT code often skip the barrier for?

                Choose an answer to see the explanation.

              7. Question 7 of 7Does your app code normally call a write barrier?

                Choose an answer to see the explanation.

              Key takeaways

              • A minor collection is fast because it looks at young memory, not all of old memory.
              • So every old-to-young pointer needs a note. Otherwise a young object that is still in use could be freed.
              • Remembered sets, store buffers, and card tables are different ways to keep or sort those notes.
              • Card marking gives up the exact slot. In return, each write only sets one flag, and the collection checks the whole dirty card later.
              • JIT code adds small checks to writes, but skips them when it can prove a write is safe.
              • Your app code never calls a barrier. If something is slow, measure it with a profile first.

              Remember the one-liner.
              A write barrier leaves a note when old memory starts pointing at young memory.

              Coming next: Incremental, concurrent & parallel GC. There, collectors split their work into small steps or run beside your code, so your page doesn't freeze in one long pause.

              CompleteFrontend Clear concepts. Working examples.