Generational collection & the Scavenger
Learn why JavaScript engines collect young objects quickly, copy survivors, promote long-lived data, and reserve major work for later.
- 01Explain why new objects are checked oftenSay why engines check new objects often and leave lasting objects alone most of the time.
- 02Follow one copying collectionTrace the roots, to-space, the scan number, an object's age, and its move to old space in a small example.
- 03Tell the two kinds of collection apartTell a minor collection (Scavenge) from a major one (Mark-Compact), and pick good habits for your own code.
Why objects die young
JavaScript programs make many small objects that are only needed for a moment. Some examples:
- a message built to show one line of text,
- the details of one click,
- an object made inside a function and used only in that call.
Soon, no part of the program can reach these objects any more. We say they are unreachable. An unreachable object is garbage, and the garbage collector frees its memory.
The engine is the program that runs your JavaScript, such as V8 in Chrome and Node. The engine keeps all objects in one memory area called the heap. Most objects in the heap die soon, so the engine treats new objects and old objects differently.
Generational collection splits the heap by age. It checks new objects often. It moves the objects that last into an older area. It checks that older area less often.
This lesson follows Mark-sweep & mark-compact. That lesson showed how a collector finds the objects that are still needed. This lesson asks a different question: which part of the heap should the collector check, and how often?
The generational hypothesis
To allocate an object means to make it and give it memory. The generational hypothesis is a pattern people have seen in real programs: most newly allocated objects become unreachable soon. It is a common pattern, not a rule of JavaScript. For example:
- a shopping cart is often kept for a long time,
- a message made while adding tea is usually needed only once.
const cart = []; function addTea() { const message = { item: "tea", qty: 1 }; cart.push("tea"); return message.item;} console.log(addTea());console.log(cart.length);- Line 1 makes
cartoutside the function. Code can still reach it afteraddTeaends, so it can last the whole shopping visit. - Line 4 makes
messageinside the function. - Line 6 returns only the text
"tea", not themessageobject. WhenaddTeaends, nothing can reachmessageany more. - Lines 9 and 10 print
tea, then1.
The word most matters. The engine does not know what an object is for. A name like receipt means nothing to it. The engine only knows two things:
- whether an object can still be reached,
- that most new objects die before the next check.
function makeReceipts(times) { let diedYoung = 0; for (let index = 0; index < times; index += 1) { const receipt = { item: "tea", index }; diedYoung += receipt.item === "tea" ? 1 : 0; } return diedYoung;} console.log(makeReceipts(3));- Line 1:
timessays how many times the loop runs. - Line 4 makes one small
receiptobject each time. Nothing keeps it after that loop turn. - Line 5 adds 1 to
diedYoungfor each receipt. In this lesson, "died young" means the object was not needed for long. - Line 10 prints
3.
This code only counts receipts to show the idea. It does not look at the real memory of your browser.
Think of the Downloads folder on your phone. You open most files there once, and then you delete them. You move the few you want to keep into a Photos album. Downloads is small, so you can clean it quickly and often. The album needs tidying much less often.
- In real life: A download you open once
- In JavaScript: A new object that is needed once
- In real life: A photo you keep
- In JavaScript: An object that lasts and moves to the old area
- In real life: Clean Downloads often
- In JavaScript: Check the new objects often
- In real life: Tidy the album rarely
- In JavaScript: Check the old area less often
Young and old generations
A generation is a group of objects of about the same age. The heap has two of them:
- The young generation, or young space, is a small area where new objects are made. The collector checks it often, because most objects there die quickly.
- The old generation, or old space, holds objects that lasted through earlier checks of young space. The collector checks it less often.
The code below acts out the two spaces with two plain arrays.
const youngSpace = ["message", "receipt"];const oldSpace = ["cart"]; console.log("young " + youngSpace.length);console.log("old " + oldSpace.length);- Line 1 puts the names of two new objects in the
youngSpacearray. - Line 2 puts the lasting
cartin theoldSpacearray. - Lines 4 and 5 print
young 2andold 1.
The arrays are only names for the idea. A real JavaScript array cannot choose where the engine keeps an object.
message and receipt sit in young space, which is cleaned often. The cart that lasts sits in old space, which is cleaned less often.Why split the heap at all? It saves work. A check of young space looks at a small area and leaves the big, lasting objects alone, such as the cart or the app's data. Young space is also cheap to clean: the collector skips the dead objects and copies only the few that are still needed.
Age does not change which objects are still needed. A young object can still be in use. An old object can later become garbage. Age only decides which area the collector usually checks first. Whether an object stays is still decided by whether the program can reach it.
Semi-space copying and Cheney's algorithm
Semi-space copying is a way to clean young space. "Semi" means half. Young space is split into two equal halves:
- from-space holds the young objects right now,
- to-space is empty and waits for the objects that are still needed.
The collector starts at the roots. A root is a place the program can always use, such as a global variable or a variable in a function that is still running. From the roots, it follows each link to find the objects that are still needed. It copies each one into to-space. An object that is copied is called a survivor. Then it throws away all of from-space in one go. It never has to free each dead object one by one.
cart, and cart reaches tea, so both are copied to to-space. Nothing reaches message, so it stays behind and is thrown away with all of from-space.This replays the lesson's own copying code on small pretend objects. It does not look inside your browser's memory.
script
const fromSpace = [ { id: "cart", age: 0, children: ["tea"] }, { id: "tea", age: 0, children: [] }, { id: "message", age: 0, children: [] },]; function scavenge(roots, fromSpace, oldSpace = []) { const byId = new Map(fromSpace.map((item) => [item.id, item])); const toSpace = []; let scan = 0; for (const id of roots) copy(id); while (scan < toSpace.length) { for (const child of toSpace[scan].children) copy(child); scan += 1; } function copy(id) { const item = byId.get(id); if (!item || toSpace.some((copy) => copy.id === id)) return; const next = { ...item, age: item.age + 1 }; if (next.age >= 2) oldSpace.push(next); else toSpace.push(next); } return { fromSpace: [], toSpace, oldSpace };} console.log(scavenge(roots, fromSpace).toSpace.map((item) => item.id).join(","));Cheney's algorithm is a simple plan for this copying, named after the person who wrote it down. It works like this:
- Copy the objects that the roots point to.
- Keep a number called
scan. It marks the next copied object to look inside. - Look inside that object. Copy any object it points to that is not copied yet.
- Add 1 to
scan. Repeat untilscanhas passed every copied object.
This order is called breadth-first: the collector finishes the objects it already has before it goes deeper. In this lesson's code, each copy also adds 1 to the object's age.
Two links can point to the same object. The collector must copy it only once. So after the first copy it writes down the new place. That note is called a forwarding address.
const fromSpace = [ { id: "cart", child: "tea" }, { id: "tea", child: null },];const forwarding = new Map(); function copyOnce(id) { if (forwarding.has(id)) return forwarding.get(id); const address = "to-" + forwarding.size; forwarding.set(id, address); return address;} console.log(copyOnce("cart"));console.log(copyOnce("cart"));- Line 5 makes a
Mapcalledforwarding. AMapstores pairs: here, an object's name and its new place. - Line 8 checks whether that name already has a new place. If it does, the function returns that place and copies nothing.
- Line 9 makes a new place only on the first copy. It is
"to-"plus how many objects were copied so far. - Lines 14 and 15 both print
to-0. Two calls forcartgive one copy, not two.
The scan number is what makes the work happen in order. When scan has passed every copied object, every young object the program can still reach is in to-space.
const roots = ["cart"];const fromSpace = [ { id: "cart", age: 0, children: ["tea"] }, { id: "tea", age: 0, children: [] }, { id: "message", age: 0, children: [] },]; function scavenge(roots, fromSpace, oldSpace = []) { const byId = new Map(fromSpace.map((item) => [item.id, item])); const toSpace = []; let scan = 0; for (const id of roots) copy(id); while (scan < toSpace.length) { for (const child of toSpace[scan].children) copy(child); scan += 1; } function copy(id) { const item = byId.get(id); if (!item || toSpace.some((copy) => copy.id === id)) return; const next = { ...item, age: item.age + 1 }; if (next.age >= 2) oldSpace.push(next); else toSpace.push(next); } return { fromSpace: [], toSpace, oldSpace };} console.log(scavenge(roots, fromSpace).toSpace.map((item) => item.id).join(","));to-space: cart,tea
old space: empty
To-space now holds cart and tea. Nothing points to message, so it is never copied.
You have two lunch boxes. Box A is full. You move only the food you will still eat into box B. Some of it is in bags, so you check each moved bag for more food to keep. Then you wash box A all at once. You do not clean each empty spot by itself.
- In real life: Box A is full
- In JavaScript: from-space is full
- In real life: Move the food you will eat
- In JavaScript: Copy the objects still needed
- In real life: Wash box A all at once
- In JavaScript: Throw away all of from-space
- In real life: Check each moved bag for more food
- In JavaScript: Look inside copied objects for more links
Promotion, tenuring, and pretenuring
Each object has an age: how many checks of young space it has survived. Tenuring means keeping track of that age. When an object is old enough, it moves to old space. That move is called promotion.
In this lesson's code, an object moves to old space when its age reaches 2. Real engines pick their own rules. The reason is the same everywhere: copying a lasting cart again and again inside young space is wasted work.
const survivor = { id: "cart", age: 1 };survivor.age += 1;const destination = survivor.age >= 2 ? "old space" : "to-space"; console.log(destination);console.log(survivor.age);- Line 1 makes a
cartobject with age1. It has survived one check. - Line 2 adds 1 to the age: it survived one more check.
- Line 3 picks
"old space", because the age is now 2. The? :works like a short if/else: if the test before?is true, it gives the first value, otherwise the second. - Lines 5 and 6 print
old spaceand2.
cart starts at age 1. It survives one more check of young space, and its age becomes 2. In this lesson's code, age 2 means it moves to old space.An allocation site is the place in your code that makes an object, such as one line with an object literal. Pretenuring means the engine notices that one allocation site usually makes objects that last. So it makes those objects straight in old space and skips young space. For example:
- the code that builds the cart for each visit may be a good fit for old space,
- a receipt made for one click is not.
function chooseSpace(survivesOften) { return survivesOften ? "old space" : "young space";} console.log(chooseSpace(true));console.log(chooseSpace(false));- Line 1:
survivesOftensays whether objects from this place usually last. - Line 2 returns
"old space"if they do, and"young space"if they do not. - Lines 5 and 6 print
old spaceandyoung space.
Pretenuring is something the engine does by itself. Your code cannot ask for it, and the engine makes no promise. It can change its choice when your program starts to behave differently. Your job stays the same:
- make it clear which code owns each lasting object,
- remove links that keep objects alive by mistake,
- measure before you change how your code makes objects.
Sharing the copying work
A thread is a worker inside the engine that can do a job at the same time as other workers. Parallel means "at the same time". Evacuate means move the objects that are still needed out of an area.
Parallel scavenging lets several helper threads copy survivors at the same time, when the engine can do it safely. V8 can also split its mark-evacuate work this way: it finds the objects still needed, then moves them. The point is sharing the work. There is no fixed number of helpers.
const survivors = ["cart", "tea", "user", "note"];const helpers = [[], []]; for (const [index, id] of survivors.entries()) { helpers[index % helpers.length].push(id);} console.log(helpers[0].join(","));console.log(helpers[1].join(","));- Line 1 lists four survivor names.
- Line 2 makes two empty work lists, one for each helper.
- Lines 4 and 5 hand out the names in turn: first helper, second helper, first, second.
survivors.entries()gives each name with its position,index.index % helpers.lengthis the remainder after dividing by 2, the number of helpers, so it goes 0, 1, 0, 1. - Lines 8 and 9 print
cart,userandtea,note.
This code runs one step at a time. It only shows how work can be split. It does not start any real threads.
Real helpers must work together carefully. Each helper must:
- claim an object before it copies it,
- write down the object's forwarding address,
- keep checking for more work.
These steps stop two helpers from moving the same object twice.
Sharing the work can make a check finish sooner when there are many survivors. It does not remove every pause. When the check is tiny, the extra teamwork can even cost more than it saves.
Minor and major collections
A collection is one run of the garbage collector. There are two kinds:
- A minor collection checks young space only. V8 calls it a Scavenge. It happens often and is usually fast, because young space is small on purpose and most objects in it are not copied.
- A major collection can check the whole heap, old space too. V8 calls it Mark-Compact, the method from the last lesson. It happens less often and can do more work.
const events = ["minor", "minor", "minor", "major", "minor"];const counts = { minor: 0, major: 0 }; for (const event of events) { counts[event] += 1;} console.log("minor " + counts.minor);console.log("major " + counts.major);- Line 1 lists four
"minor"events and one"major"event. - Line 2 starts both counters at 0.
- Line 5 adds 1 to the counter whose name matches the event.
- Lines 8 and 9 print
minor 4andmajor 1.
This replay counts made-up events. A real engine picks when to collect, based on how fast your code makes objects.
script
const counts = { minor: 0, major: 0 }; for (const event of events) { counts[event] += 1;} console.log("minor " + counts.minor);console.log("major " + counts.major);The table puts three easy-to-mix-up words side by side. Read each row from left to right.
| Word | What does it look at or do? | How often? | What does V8 call it? |
|---|---|---|---|
| Minor collection | It checks young space only. | Often. It is usually fast. | V8 calls it a Scavenge. |
| Major collection | It can check the whole heap, old space too. | Less often. It can do more work. | V8 calls it Mark-Compact. |
| Promotion | It moves a survivor from young space to old space. | Once the object has survived enough checks of young space. | It is part of tenuring: keeping track of an object's age. |
What Node shows about V8
Node is a program that runs JavaScript outside the browser. It uses V8, the same engine as Chrome. Node can show some of V8's memory details, which helps when you look for problems.
The two snippets below only work in Node, so you cannot run them on this page. The lesson test checks only names and version numbers. It never treats memory sizes or times as fixed facts, because they change from machine to machine.
import v8 from "node:v8";
console.log(process.version);
console.log(process.versions.v8);
console.log(v8.getHeapSpaceStatistics().map((space) => space.space_name).join(","));const keep = [];
for (let index = 0; index < 30000; index += 1) {
keep.push({ item: "tea", index });
}
console.log("kept", keep.length);- The first snippet asks V8 for its memory areas. On Node 22, which uses V8 12.4, the names include
new_space(young space) andold_space(old space). - The second snippet makes 30,000 small objects. Run it with the
--trace-gcoption, which tells Node to print a line for each collection. You will see lines with the wordScavenge.
These words show that the ideas in this lesson are real in V8. They are not promises about speed.
Habits for your own code
Do not write app code that counts on collections happening at a certain time. Instead, follow these habits:
- keep objects that are needed once inside the function,
- do not add data to global caches by accident,
- remove event listeners and timers that keep data alive when you no longer need them.
An object that lasts a long time is fine, as long as it is clear which part of the code owns it.
- The
messageobject made insideaddTea - A cart kept for the whole shopping visit
- Copy the young objects still needed into to-space
- An object that survived two checks of young space
- A new object that nothing can reach
- Mark and compact the whole heap
Put each card in the group that fits it best, using this lesson's cart and message examples.
Common misconceptions
- "Young means unreachable." Young only means the object was made recently. A young object can still be in use.
- "Promotion means it lives forever." An object in old space can still become garbage. A later major collection frees it.
- "A minor collection never moves objects." A Scavenge copies the survivors to a new place, so it does move them.
- "Pretenuring is something my code can call." It is a choice the engine makes by itself, based on what it sees your code do.
Practice exercises
Type the two values this code prints, in order.
const cart = ["tea"];
function makeMessage() {
const message = { item: "tea", qty: 1 };
return message.item;
}
console.log(makeMessage());
console.log(cart.length);It prints tea and then 1. The message object is needed only inside the function. The cart can still be reached after the call.
Which object ends up in to-space?
const result = scavenge(["cart"], [
{ id: "cart", age: 0, children: [] },
{ id: "message", age: 0, children: [] },
]);
console.log(result.toSpace.map((item) => item.id).join(","));Only cart is copied. message stays in from-space and is thrown away with it later.
In this lesson's code, where does an object go after it survives its second check of young space?
After it survives a second time, its age is 2, so this lesson's code moves it to old space.
A report builder makes objects that stay for the whole visit. What can the engine choose to do with the objects it makes?
The engine can pretenure: make the objects from that place straight in old space.
In a busy page with lots of clicks and typing, which collection would you expect to run more often?
The minor collection, which V8 calls a Scavenge, runs most often.
Typing in a text box feels slow. What should you do before you change every { } object in the code?
Measure first. Then use what you find to decide whether making many small objects is really the problem.
Check your understanding
Think about the cart, the message, and the two spaces as you answer. They help keep the words apart.
Question 1 of 6What does the generational hypothesis say?
Choose an answer to see the explanation.
Question 2 of 6What does this print?
Read the code, then predictconst cart = ["tea"]; console.log(cart.length);Choose an answer to see the explanation.
Question 3 of 6In semi-space copying, what happens to a young object that nothing can reach?
Choose an answer to see the explanation.
Question 4 of 6In Cheney's algorithm, what is the
scannumber for?Choose an answer to see the explanation.
Question 5 of 6What is pretenuring?
Choose an answer to see the explanation.
Question 6 of 6Which sentence about minor and major collections is true?
Choose an answer to see the explanation.
Key takeaways
- Most new objects die young, so the engine checks young space often.
- Semi-space copying copies the objects still needed to to-space, then throws away all of from-space at once.
- Objects that survive enough checks move to old space. With pretenuring, the engine can make lasting objects in old space right away.
- A minor collection (V8: Scavenge) checks young space only. A major collection (V8: Mark-Compact) can check the whole heap.
Remember the one-liner.
Generational collection makes the many objects that are needed only once cheap to clean up.
Coming next: Write barriers & remembered sets. It shows what happens when an object in old space points to an object in young space.