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 the young-object shortcutDescribe why engines collect a young generation often while leaving long-lived objects alone.
- 02Trace a copying collectionFollow roots, to-space, the scan index, survivor age, and promotion in a small Cheney-style model.
- 03Compare collection workSeparate minor Scavenge collections from major Mark-Compact work and identify practical allocation choices.
Why objects die young
JavaScript programs create many small, temporary objects. A formatted message, an event payload, or an object literal inside a function often becomes unreachable before the next screen update. Engines exploit that common pattern instead of treating every object as equally likely to live for hours.
Generational collection divides the heap by age. It collects new objects often, moves survivors to an older area, and visits that older area less often.
This continues Mark-sweep & mark-compact. That lesson explains how a collector finds live objects. This lesson asks a simpler question first: where should it spend that work?
The generational hypothesis
The generational hypothesis is the observed pattern that most newly allocated objects become unreachable soon. It is a useful tendency, not a JavaScript guarantee. A cart can be long-lived; a message created while adding tea is usually short-lived.
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 creates cart outside the function, so it can stay reachable for the shopping session. Line 4 creates message. Line 6 returns only its item text, so the message object can die when addTea returns. The code prints tea, then 1.
The word most matters. The engine does not know that a receipt is unimportant just because it has that name. It learns only from reachability and from the repeated pattern that many new values disappear before the next young collection.
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 accepts the number of receipt-making turns. Line 3 makes one small receipt each turn. Line 5 counts it as a young death in this teaching model, and line 9 prints 3. This is a model count, not a browser heap measurement.
Most files in a phone Downloads folder are opened once and deleted soon. The few you keep move into a Photos album. You can clean Downloads quickly and often, while the album needs attention less often.
- In real life: A download used once
- In JavaScript: A young temporary object
- In real life: A saved photo
- In JavaScript: A long-lived promoted object
- In real life: Clean Downloads often
- In JavaScript: Run minor collection often
- In real life: Open Photos less often
- In JavaScript: Visit old space less often
Where the analogy stops: Folders are visible choices. The engine uses reachability and collection policies, not your personal judgment.
Young and old generations
The young generation is the small area for new allocations. It is collected often because most objects there die quickly. The old generation holds objects that already survived young collections, so it is not scanned for every small burst of temporary work.
const youngSpace = ["message", "receipt"];const oldSpace = ["cart"]; console.log("young " + youngSpace.length);console.log("old " + oldSpace.length);Line 1 puts temporary names in the young-space array. Line 2 puts the durable cart in old space. Lines 4 and 5 print young 2 and old 1. The arrays are labels for a teaching model; JavaScript arrays do not choose a real heap generation.
The split is a cost decision. A frequent young collection can inspect a small area and leave a large, stable cart or app state alone. A small young space is cheap to clean because dead values are skipped and the few live values are copied.
This does not change JavaScript reachability. A young object can be live, and an old object can later be garbage. Age decides which collection usually looks first; root paths decide whether the object survives.
Semi-space copying and Cheney's algorithm
Semi-space copying reserves two young halves: from-space contains current objects and to-space receives survivors. Rather than freeing each dead message, the collector copies reachable survivors and discards the entire old half in one go.
This is a replay of an instrumented teaching model. It copies real model objects; it does not inspect your engine's heap.
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 breadth-first copying plan. It keeps a scan index over objects already copied to to-space. Each scanned survivor can reveal children that also need copying. The model increments age as it copies.
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 creates a forwarding map. Line 8 checks whether cart already has a to-space address. Line 9 makes an address only on the first copy. Lines 13 and 14 both print to-0, which shows why two references to one object do not create two copies.
The scan index is what makes the work breadth-first. First copy roots. Then scan the first copied object, copy its children, advance scan, and repeat until every copied object has been scanned. At that point every reachable young object is in the destination half.
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
The next to-space contains cart and tea. The temporary message is never copied.
When lunch box A is full, move only the food you will still eat into lunch box B. Then wash box A in one go. You do not need to scrub each empty spot separately.
- In real life: Box A is full
- In JavaScript: from-space is full
- In real life: Move food you will eat
- In JavaScript: Copy reachable survivors
- In real life: Wash Box A at once
- In JavaScript: Discard all old from-space
- In real life: Check moved food for smaller food
- In JavaScript: Scan copied children
Where the analogy stops: Food does not point to other food. Heap objects can reference more objects, which is why the scan index matters.
Promotion, tenuring, and pretenuring
Tenuring means tracking how many young collections an object survives. In this teaching model, an object moves to old space after its second survival. Real engines choose details differently, but the reason is the same: repeatedly copying a durable cart wastes young-collection 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 starts a cart at age 1. Line 2 records one more survival. Line 3 chooses old space because age is now two. Lines 5 and 6 print old space and 2.
Pretenuring means an engine may notice that one allocation site usually creates long-lived objects and allocate those objects directly in old space. For example, a site that builds the persistent cart state for every session may be a better old-space candidate than a receipt created for one click.
function chooseSpace(survivesOften) { return survivesOften ? "old space" : "young space";} console.log(chooseSpace(true));console.log(chooseSpace(false));Line 1 asks one clear question about an allocation site. Line 2 returns old space when its objects usually survive. Lines 5 and 6 print old space and young space.
Pretenuring is not an API and it is not a promise. Application code should still make ownership clear, remove accidental retainers, and measure before changing allocation patterns. The engine can revise its allocation choice when the program changes.
Parallel mark-evacuate
Parallel scavenging lets several helper threads copy survivors at the same time when the engine can do so safely. V8 can use parallel mark-evacuate work too. The point is shared work, not a promise about one fixed number of threads.
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 names four survivors. Line 2 makes two work lists. Lines 4 and 5 give alternating names to the two lists. Lines 8 and 9 print cart,user and tea,note. This is a sequential JavaScript model of divided work, not a thread-count probe.
Real helpers need shared coordination. They must claim a survivor before copying it, publish its forwarding address, and keep checking for more work. That extra machinery prevents two helpers from moving the same object twice.
Parallel work can reduce elapsed collection time when enough survivors exist. It does not erase all pause costs, and it can add coordination overhead when the young collection is tiny.
Minor and major collections
A minor collection touches the young generation only. V8 calls this a Scavenge. It is frequent and usually fast because the young area is deliberately small and most objects are not copied.
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 contains four minor events and one major event. Line 5 increases the matching counter. Lines 8 and 9 print minor 4 and major 1.
This replay counts model events. A real engine chooses collection timing based on current allocation pressure.
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);| Kind | Area | Typical rhythm | V8 name |
|---|---|---|---|
| Minor collection | Young generation only | Frequent and usually fast | Scavenge in V8 |
| Major collection | Whole heap, including old space | Rarer and can do more work | Mark-Compact in V8 |
| Promotion | Move a survivor to old space | After it survives enough young collections | Tenuring |
Small V8 evidence
Node exposes V8 information for diagnosis. These snippets are Node-only, so they are not browser playgrounds. The lesson test asserts names and version families only; it never treats heap sizes or timings as stable facts.
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);On Node 22 with V8 12.4, heap statistics include new_space and old_space. Running the second snippet with --trace-gc prints Scavenge lines. Those words are evidence of the concepts, not performance promises.
Practical allocation habits
Do not write app code around one assumed collection schedule. Keep temporary values local, avoid accidental global caches, and remove listeners or timers that retain data. A long-lived object is fine when it belongs to a clear owner.
- The
messageobject insideaddTea - A cart kept for the whole shopping session
- Copy reachable young objects to to-space
- An object that survived two young collections
- A temporary object with no root path
- Mark and compact across the whole heap
Sort each card by the role it has in this lesson's cart and message examples.
Common misconceptions
- “Young means unreachable.” Young means recently allocated. A young object can be live.
- “Promotion means permanent.” Old objects can still become garbage and be collected later.
- “Minor means no moving.” A copying Scavenge moves survivors.
- “Pretenuring is an API.” It is an engine choice based on observed allocation behavior.
Practice exercises
Type the two output values from the tea example.
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 temporary; the cart remains reachable.
Which object reaches 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(","));The model copies cart. It leaves message in from-space and discards that space later.
Where does a survivor go after its second young collection in this lesson model?
After the second survival, the model promotes the object to old space.
A report builder creates objects that stay for the entire session. What can the engine choose to do?
An engine can pretenure: allocate that site's objects directly in old space.
Which collection would you normally expect more often in a busy UI?
Minor collection, called Scavenge in V8, is expected most often.
A typing experience feels slow. What should you do before changing every object literal?
Measure first. Then use the evidence to decide whether temporary allocation is really the problem.
Check your understanding
Use the cart, message, and two-space model to keep the terms separate.
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 an unreachable young object?
Choose an answer to see the explanation.
Question 4 of 6What is Cheney's scan index 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 statement compares collections accurately?
Choose an answer to see the explanation.
Key takeaways
- Most new objects die young, so engines collect young space often.
- Semi-space copying moves survivors and discards the whole old half.
- Survivors may be promoted to old space; pretenuring can skip young space for likely durable objects.
- Minor Scavenge work is young-only; major Mark-Compact work can cover the heap.
Remember the one-liner.
Generational GC makes the common short-lived object cheap to clean up.
Coming next: Write barriers & remembered sets, where old objects point to young ones.