The Heap: Young Space and Old Space
Create a million objects inside a loop and throw every one of them away. It barely costs a thing. Now leak a few thousand into a cache you forgot to bound, and your app gets slower over the afternoon. That reads backwards. A million should hurt more than a few thousand.
It does not, and the reason is the most consequential decision in the engine’s memory manager. The heap is not one big pool of memory where every object is treated the same. V8 splits it by age. Almost everything about how much an allocation costs, and how fast the collector can clean up, falls out of that one split.
You already know how the collector decides what is garbage: an object lives while it is reachable from a root, and it dies when nothing can reach it anymore. That is the what. This lesson is the where. Where do the objects you create actually live, and why does the engine bother sorting them by age before collecting them.
Most objects die young
Watch what a normal program actually does with memory and a pattern jumps out immediately. Nearly everything you allocate is a flash in the pan. A temporary object inside a function, the intermediate array from a .map(), the arguments to a call, a short-lived closure: born, used for a few operations, gone. A small minority of what you allocate sticks around: the app’s core state, a long-lived cache, the module singletons wired up at startup. And here is the second half of the pattern, the part that makes the whole design work. The objects that have already survived a while tend to keep surviving.
Plot the lifetimes and you get a shape like this every time.
This is the generational hypothesis, and it is the single observation the whole memory system is built on. Most objects die young. The ones that make it past infancy tend to be long-lived. It is not a law of the language, it is an empirical fact measured across huge numbers of real programs, and it holds for almost all of them, including yours.
If that is true, a collector that treats a five-millisecond temporary and a five-hour cache entry identically is doing a lot of pointless work. Every collection re-examines the long-lived objects that were never going to die, just to find the fresh corpses mixed in with them. The fix writes itself. Sort objects by age, and spend your effort where the death rate is high.
Two generations
So V8 divides the heap into two main regions, and where an object sits tells the collector how to treat it.
The young generation, which V8 calls new space, is small and it is where almost every object is born. It is collected constantly, and because it is small and mostly dead by the time the collector looks, cleaning it is quick. New space is split internally into two equal halves called semi-spaces, which is how its collector works. Only one half is in use at a time. We will get to why in a moment.
The old generation, old space, is much larger and holds the objects that have proven they stick around. It is collected far less often, because its residents rarely die. When it is collected the work is heavier (marking a large live set, then squeezing out the gaps), so V8 does it infrequently and hides the cost, which is the subject of the next lesson.
Those two do the vast majority of the work, but they are not the only regions. A few specialised pools sit alongside them, each collected on its own terms:
- Large-object space holds allocations too big to fit in a normal page (a huge array, a big buffer). These objects get their own memory and are never moved around, because copying something that large would defeat the purpose.
- Code space holds the machine code the JIT compilers generate.
- Read-only space holds immutable engine internals that can be shared and never need collecting.
You rarely think about those. The young/old split is the one that shapes how your code performs, so that is where we stay.
Allocation is a pointer bump
Here is the first payoff of keeping new space small and simple: putting a new object into it is almost free.
New space is one contiguous stretch of memory with a single marker, an allocation pointer, sitting at the boundary between “used” and “free”. To allocate an object, V8 writes it at the pointer and then moves the pointer forward by the object’s size. That is the entire operation. No scanning a free list for a hole big enough, no bookkeeping to find a slot. Just write, and bump.
This is why the fear of “creating too many objects” is mostly misplaced. Allocating a short-lived object is one of the cheapest things the engine does. The loop below spins up a million tiny objects, and the allocation cost per object is close to negligible, because each one is just a pointer bump in new space, and the whole batch dies together and gets swept away for almost nothing:
let total = 0;
for (let i = 0; i < 1_000_000; i++) {
const point = { x: i, y: i * 2 }; // born, read once, dead
total += point.x + point.y;
}
There is a companion cost worth knowing, covered in the numbers lesson: a small integer needs no allocation at all, while a boxed value on the heap does. But for genuine objects, the young-space allocator is about as fast as allocation gets. Cheap to make, and as you are about to see, cheap to reclaim.
Surviving: promotion to old space
New space fills up fast, because everything lands there. When it does, V8 runs a quick young-generation collection (the Scavenger, whose mechanics are the next lesson). The key fact for right now: that collection is cheap precisely because of the hypothesis. Most of new space is already garbage, so there is little to save. The collector copies out the few live objects and declares the rest of the space free in one stroke. It never even looks at the dead ones.
An object that survives one of these collections has beaten the odds once. V8 does not promote it yet. It gives the object a second chance, keeping it in the young generation as an “intermediate”. Survive a second young collection and V8 makes its bet: this object is one of the long-lived ones, and it gets promoted (the term of art is tenured) into old space, where the collector will mostly leave it alone.
The logic is a direct bet on the hypothesis. An object that has already lived through two collections is, statistically, exactly the kind of object that goes on living. So V8 moves it out of the busy young generation, where it would otherwise be copied over and over on every scavenge, and into old space, where it will be checked only rarely. Promotion is the engine saying “you have earned a quieter neighborhood”.
Old space, then, is a slow-moving city of long-lived residents, filled by the trickle of young objects that keep proving themselves. Because those residents rarely die, V8 can afford to collect old space infrequently, using a heavier algorithm (marking the live set, then compacting to remove the gaps) that would be far too slow to run on every allocation.
Watch the generations work
The demo below is a simulation, not a real heap, but it moves the way the real thing does. Allocate a batch of objects and they land in young space, each with a lifespan drawn from a young-skewed distribution (most are doomed). Run a young collection and watch the crowd thin out: the doomed ones vanish for almost nothing, the survivors age, and any object that lives through two collections is promoted into old space. Run it a few times and the shape of the hypothesis appears on its own.
Notice the ratio the “died young” percentage settles into. The overwhelming majority of what you allocate never makes it to old space, which is exactly why a collector that focuses on young space can be fast. It is doing a small amount of work in the one place where almost all the death happens.
What this means for your code
Step back from the machinery and the practical picture is short.
Short-lived objects are cheap on both ends. Cheap to allocate (a pointer bump) and cheap to reclaim (a scavenge barely touches the dead). So the temporary object that makes a function readable, the intermediate array in a chain, the small closure you return, none of these are the performance sin folklore makes them out to be. Write the clear version.
The thing to actually worry about is the opposite: objects that survive by accident. An entry pushed into an array that never gets trimmed. A cache with no bound. A listener that keeps a closure alive long after it is needed. These objects do exactly what a real long-lived object does (they survive collections), so V8 tenures them into old space and stops watching them closely. They accumulate there, quietly, and old space grows.
const cache = [];
function handle(req) {
const entry = { id: req.id, body: req.body };
cache.push(entry); // never trimmed → survives, gets tenured, never leaves
}
You can see the difference in the memory graph. Healthy generational memory has a distinctive sawtooth: usage climbs as you allocate, then drops sharply each time a young collection reclaims the short-lived flood, over and over. Underneath the teeth is a floor, which is old space. When nothing leaks, that floor stays roughly flat. When something survives by accident, the floor creeps upward collection after collection, and that slow climb is the signature of a leak.
That sawtooth is not a problem to fix. It is the collector doing its job well, and a graph with no teeth at all would be the strange one. The graph to fear is the one where the floor never comes back down. Chasing that floor, reading a heap snapshot to find what is stuck in old space that should not be, is the whole subject of finding leaks. And when a collection does cause a visible hiccup, a dropped frame or a janky interaction, the pause almost always comes from old space, which ties back to responsiveness metrics and to how V8 hides those pauses in the next lesson.
Summary
- V8 does not treat the heap as one pool. It splits it by age into a small young space (new space), where objects are born, and a large old space, where survivors live.
- The design rests on the generational hypothesis: most objects die young, and the ones that survive a while tend to keep surviving. This is empirical and holds for almost every program.
- Allocation in young space is a pointer bump: write the object, move the pointer. That is why creating short-lived objects is cheap, and why young collections are cheap too, since they only copy the few survivors.
- Objects that live through a couple of young collections are promoted (tenured) into old space, on the bet that they are long-lived. Old space is collected rarely, with a heavier algorithm. Exact thresholds are engine internals and change.
- Other regions (large-object, code, read-only space) exist but rarely shape day-to-day performance.
- What this means for your code: short-lived allocations are cheap, so favor clear code over premature pooling. The real risk is objects that survive by accident. A sawtooth memory graph is healthy; a floor that keeps climbing is a leak. You steer all of this only by controlling how long you hold references.
Next, the collection mechanics themselves: how the Scavenger empties young space, how Mark-Compact handles old space, and how V8 keeps both from freezing your program mid-frame.