The heap & pointer compression
Learn how V8 organizes heap spaces and pages, allocates with bump pointers, compresses heap pointers, and reports Node heap limits.
- 01Name the main heap spacesExplain why V8 keeps new objects, long-lived objects, machine code, and very big objects in separate spaces, not in one big pile.
- 02Follow how objects fill a pageStep through a bump allocator that places objects one after another on a page, and moves to a new page when the next object does not fit.
- 03Explain compressed addressesDescribe how V8 stores an address as a 4-byte offset inside a 4 GB cage, why Node.js is built differently from Chrome, and what the V8 sandbox protects.
The heap is organized space
In the lessons on stack and heap and memory management, you learned a simple picture: objects live in a big area of memory called the heap. This lesson looks inside the heap of V8, the JavaScript engine in Chrome and Node.js. An engine is the program that runs your JavaScript.
V8 does not keep the heap as one big pile. It splits it into areas and pieces. This lesson shows how, and why.
The heap layout is how the engine divides its heap into areas. In V8:
- The heap is split into spaces, such as new space, old space, and code space.
- Each space is split into fixed-size pieces called pages. Most small objects go on these pages.
- Very big objects go to a separate large-object space.
- Pointer compression stores an object's address in 4 bytes instead of 8, as a distance from a fixed starting address.
Two topics are left to other lessons. How V8 stores small numbers inside values is in the value-representation lesson. How V8 finds and frees unused objects (garbage collection) is in the garbage-collection lessons, starting with gc-basics. This lesson asks only two questions:
- Where does the engine put each object?
- Why does V8 sometimes store an address in half the usual space?
This lesson comes after Prototypes inside the engine. The next lesson, Stack frames & calling conventions, moves from the heap to the stack that function calls use.
New, old, code, and large-object spaces
A space is one area of V8's heap with one job. These are the main ones:
- New space holds new, small objects. Most objects start here.
- Old space holds objects that stayed in use for a while. V8 moves them here from new space.
- Code space holds machine code: instructions the computer can run directly. V8 makes this code from your JavaScript to run it faster. It needs different safety settings, so it is kept apart.
- Large-object space holds objects that are too big for the normal pieces of a space.
You can list the spaces yourself in Node.js. The built-in node:v8 module reports facts about V8. Its getHeapSpaceStatistics() function returns one entry per space, and each entry has a space_name. This code only runs in Node.js, not in a browser page.
const v8 = require("node:v8");const names = v8.getHeapSpaceStatistics().map((space) => space.space_name).sort();console.log(names.join(", "));The lesson test runs this code in Node.js 22, which uses V8 version 12.4. The list includes new_space, old_space, code_space, large_object_space, new_large_object_space, and code_large_object_space. The test checks only the names, because the sizes change from run to run.
Each row of this table is one space. Read across to see what it holds and why V8 keeps it apart.
| Space | What goes there | Why it is kept apart |
|---|---|---|
new_space | New, small objects. | Placing objects here is fast, and most of them are not needed for long. How V8 frees them is in the garbage-collection lessons. |
old_space | Objects that stayed in use, moved here from new space. | It holds ordinary objects that are kept a long time. |
code_space | Machine code that V8 makes from your JavaScript. | Code that the computer runs needs different safety settings, so V8 keeps it apart. |
large_object_space | Objects bigger than 128 KB, the limit for a regular page. | Each big object gets its own place, instead of being squeezed onto a regular page. |
new_large_object_space and code_large_object_space | New objects or machine code that are already big. | Node.js lists a separate large-object space for each of these. |
| Read-only, trusted, and shared spaces | Objects that V8 itself uses, with special rules. | They show up in Node.js's list, but your app's everyday objects do not start there. |
The list changes as V8 changes. This Node.js version also lists read-only, trusted, and shared spaces. These names belong to V8, not to the JavaScript language. The language rules never mention spaces.
Pages and bump allocation
A page is a fixed-size piece of a space. In current V8, a regular page is 256 KB (262,144 bytes). The biggest object that fits the normal way is half a page: 128 KB.
These numbers come from V8's own source code, which is written in C++. Each line below sets one number:
- Line 2 sets
kPageSizeBitsto 18. - Line 3 makes the page size
1 << 18. The<<sign shifts the bits of 1 to the left 18 times, which is 2 multiplied by itself 18 times: 262,144 bytes. - Line 5 makes the biggest regular object
1 << 17bytes, which is half a page.
// V8 source, src/base/build_config.h:// constexpr int kPageSizeBits = 18;// constexpr int kRegularPageSize = 1 << kPageSizeBits;// V8 source, src/common/globals.h:// constexpr int kMaxRegularHeapObjectSize = (1 << (kPageSizeBits - 1));To place a new object on a page quickly, V8 often uses bump allocation. To allocate means to give an object a place in memory. Bump allocation works like this:
- Keep a number, here called
nextFree, that marks the first free byte on the page. - Check that the new object fits between
nextFreeand the end of the page. - If it fits, place the object at
nextFree, then movenextFreeforward by the object's size. - If it does not fit, move to a new page, or take a slower path.
There is no searching for a gap. That is why it is fast.
Picture a parking lot where cars park in order. An attendant points at the first free spot. A car parks there, and the attendant points at the next spot. Nobody walks the whole lot looking for a gap.
- In real life: The first free spot is known
- In JavaScript: The allocator keeps a
nextFreenumber - In real life: A car takes that spot
- In JavaScript: A small object gets the address at
nextFree - In real life: The marker moves forward
- In JavaScript:
nextFreemoves forward by the object's size - In real life: A full row opens another row
- In JavaScript: A full page sends the next object to another page
The step-through below runs a small model of a bump allocator. It uses tiny 1024-byte pages so you can see a page fill up. It also rounds each size up to a multiple of 16 bytes (line 6), because allocators keep objects lined up on even boundaries. This is called alignment.
Watch objects fill a tiny page from left to right. When the next object does not fit, it starts a new page.
script
let page = 0;let nextFree = 0; function align16(bytes) { return Math.ceil(bytes / 16) * 16;} function allocate(label, requested) { const size = align16(requested); if (nextFree + size > pageSize) { page += 1; nextFree = 0; } const address = { page, start: nextFree, end: nextFree + size }; nextFree += size; return label + ": page " + address.page + ", bytes " + address.start + "-" + address.end;} console.log(allocate("header", 120));console.log(allocate("items", 380));console.log(allocate("names", 460));console.log(allocate("avatar", 180));avatar needs 192 bytes, but only 48 are left on page 0, so it starts page 1.Line 16 is the fast path: nextFree += size moves the marker forward. Line 12 runs only when the next object would go past the end of the page. This is a model to learn from. It does not look at your browser's real heap.
Each row of this table is one word from this section. Read across to see what it means and why it matters.
| Word | What it means | Why it matters |
|---|---|---|
| Regular page | A fixed-size piece of a space: 1 << 18 bytes, which is 256 KB, in current V8. | Many small objects share one page. |
| Biggest regular object | Half a page: 128 KB. | An object bigger than this goes to large-object space. |
| Bump pointer | A number, here called nextFree, that moves forward after each object. | It makes placing objects fast: check for room, place at nextFree, then move it forward. |
| Large-object page | A place of its own for one big object. | The big object does not have to fit on a regular page. |
Why very large objects get their own space
Some objects are bigger than 128 KB. For example, a big array needs one long stretch of memory to hold its items. That stretch is too big for a regular page. So V8 gives it a place of its own in large-object space.
Being in large-object space says nothing bad about the object. It is not a leak (memory your app forgot to let go). It is just too big for the normal path.
When you move into a flat, small boxes go up in the lift. A sofa is too big, so it goes up the stairs. The way up depends only on size, not on how much you want the sofa.
- In real life: Small items fit in the lift
- In JavaScript: Small objects fit on regular pages
- In real life: A sofa does not fit in the lift
- In JavaScript: A huge array would not fit on a regular page
- In real life: Big furniture goes up the stairs
- In JavaScript: V8 puts it in large-object space
- In real life: The sofa is still yours
- In JavaScript: The object stays as long as your code still uses it
This Node.js code checks that a big array really lands in large-object space. It uses two new tools:
global.gc()asks V8 to clean up unused objects right now. It exists only when Node.js starts with the--expose-gcoption.space_used_sizeis how many bytes a space is using.
In plain words, line by line:
- Line 7 saves how much large-object space is in use before.
- Line 8 makes an array of 20,000 items and keeps it, so it is not cleaned up.
- Line 10 reads the size again after.
- Line 11 prints whether the space grew. Line 12 prints whether it grew by at least 131,072 bytes (128 KB).
const v8 = require("node:v8");if (typeof global.gc !== "function") throw new Error("run with --expose-gc");function used(spaceName) { return v8.getHeapSpaceStatistics().find((space) => space.space_name === spaceName)?.space_used_size ?? 0;}global.gc();const before = used("large_object_space");global.big = new Array(20000).fill(1);global.gc();const after = used("large_object_space");console.log(after > before);console.log(after - before >= 131072);The test runs this code with --expose-gc and gets true twice. So large_object_space grew by at least 128 KB, the size limit for a regular object. The test checks that it grew, not the exact number of bytes.
- A tiny
{ name: "Asha" }object just made inside a click handler. - A settings object the app keeps for the whole visit.
- Machine code V8 made for a JavaScript function that runs very often.
- An array whose items take more than 128 KB.
- A small function made for one form check and then thrown away.
- A very big piece of machine code.
Put each object in the space it most likely goes to, using what this lesson taught.
Pointer compression and the 4 GB cage
Objects point at each other. For example, an array holds the addresses of its items. An address (also called a pointer) is a number that says where something is in memory. On a 64-bit computer, a full address takes 8 bytes.
V8 in Chrome saves memory with pointer compression. It works like this:
- V8 reserves one block of memory that is 4 GB big, and puts all its heap objects inside it. This block is called the cage. Its first address is the cage base.
- For each address inside the cage, V8 stores only the distance from the cage base. This distance is called the offset.
- The offset is never more than 4 GB, so it fits in 4 bytes (32 bits) instead of 8.
- When V8 needs the full address, it adds the cage base back to the offset.
The model code below works with addresses as numbers. A few tools appear in it:
- Numbers that start with
0xare written in hexadecimal (base 16), the usual way to write addresses. - The
nat the end, as in0x0000000400000000n, makes a BigInt: a number type that holds whole numbers of any size exactly. toString(16)writes a number in hexadecimal, andpadStart(8, "0")adds zeros in front until it has 8 digits.offset >>> 0turns the offset into a whole number from 0 up to 4 GB minus 1.
Follow one address: the full address, its 4-byte offset, then the base plus the offset again.
script
const cageSize = 0x0000000100000000n; function compressPointer(address) { if (address < cageBase || address >= cageBase + cageSize) { throw new RangeError("address is outside the 4 GB cage"); } return Number(address - cageBase);} function decompressPointer(offset) { return cageBase + BigInt(offset >>> 0);} const address = cageBase + 0x12345678n;const compressed = compressPointer(address);console.log("0x" + compressed.toString(16).padStart(8, "0"));console.log("0x" + decompressPointer(compressed).toString(16));const cageBase = 0x0000000400000000n;const cageSize = 0x0000000100000000n; function compressPointer(address) { if (address < cageBase || address >= cageBase + cageSize) { throw new RangeError("outside the cage"); } return Number(address - cageBase);}0x4000000000x4ffffffff0x4123456780x123456780x4123456780x412345678 is inside the cage. Its offset is 0x12345678. Adding the base back gives 0x412345678.
const cageBase = 0x0000000400000000n;const cageSize = 0x0000000100000000n; function compressPointer(address) { if (address < cageBase || address >= cageBase + cageSize) { throw new RangeError("address is outside the 4 GB cage"); } return Number(address - cageBase);} function decompressPointer(offset) { return cageBase + BigInt(offset >>> 0);} const address = cageBase + 0x12345678n;const compressed = compressPointer(address);console.log("0x" + compressed.toString(16).padStart(8, "0"));console.log("0x" + decompressPointer(compressed).toString(16));In plain words, the key lines:
- Lines 1 and 2 set the cage base and the cage size, 4 GB.
- Line 5 is the safety check: an address outside the cage cannot be stored as an offset, so the code throws an error.
- Line 8 returns the offset: the address minus the cage base.
- Line 12 turns an offset back into a full address by adding the cage base.
- Lines 17 and 18 print
0x12345678(the offset) and0x412345678(the full address again).
Each row of this table is one idea from this section. Read across to see what it stores and what to watch for.
| Idea | What it stores | What to watch for |
|---|---|---|
| Full address | The real place in memory, as a number. | It takes 8 bytes on a 64-bit computer. |
| Compressed address | Only the offset: the distance from the cage base. | It takes 4 bytes. V8 adds the base back before it uses the address. |
| The 4 GB cage | The block of memory that a 4-byte offset can reach. | Compressed addresses work only for objects inside it. It does not mean every Node.js program is set up this way. |
| Official Node.js 22 builds | process.config.variables shows 0 for pointer compression and the sandbox. | So Node.js can use a heap bigger than 4 GB. |
| Chrome on 64-bit computers | The V8 team's articles describe pointer compression and the sandbox as Chrome features. | Chrome and Node.js both use V8, but they are built with different options. |
Pointer compression also changes how small whole numbers are stored. V8 keeps a small whole number right inside the value, with no separate object on the heap. V8 calls this a Smi ("small integer"). The value-representation lesson explains Smis fully.
The code below asks V8 whether a number is a Smi. %IsSmi is a special V8 function. It works only when Node.js starts with the --allow-natives-syntax option, so this code does not run in a browser.
console.log(%IsSmi(2 ** 31 - 1));console.log(%IsSmi(2 ** 31));The official Node.js build does not use pointer compression. In it, 2 ** 31 - 1 is a Smi (line 1 prints true), and 2 ** 31 is not (line 2 prints false). Chrome on 64-bit computers uses pointer compression, and there Smis have only 31 bits for the number, as the V8 team's article on pointer compression describes.
The V8 sandbox
Pointer compression saves memory. The V8 sandbox is about safety, and it is a different thing. A sandbox is a closed area that code cannot reach out of.
Programs have bugs. A bug in V8 could let an attacker change bytes in the heap. The V8 sandbox keeps the heap closed off, so such a bug cannot reach the rest of the browser's memory. Here is how, as the V8 team's article on the sandbox describes:
- Addresses inside the sandbox are stored as offsets, like in pointer compression.
- Addresses of memory outside the sandbox are not stored in the heap at all. The heap stores a number that points into a separate list, such as the external pointer table. V8 checks that list before it uses the address.
The V8 team describes pointer compression and the sandbox as features of Chrome on 64-bit computers. Node.js also uses V8, but it turns on different options when it is built. In the official Node.js 22 builds used here, both process.config.variables.v8_enable_pointer_compression and process.config.variables.v8_enable_sandbox are 0, which means off.
process.config.variables lists the options Node.js was built with. Line 1 prints the pointer compression option. Line 2 prints the sandbox option.
console.log(process.config.variables.v8_enable_pointer_compression);console.log(process.config.variables.v8_enable_sandbox);The V8 sandbox does not replace the browser's own process sandbox, which keeps each site in a separate locked-down program. It also does not make every bug harmless. It is one more layer: an attacker who changes the heap still needs a second bug to break out and reach other memory.
Heap size limits
V8 has a top size for its heap. In Node.js, v8.getHeapStatistics().heap_size_limit reports it in bytes. You can raise or lower it with the --max-old-space-size option, which sets the biggest size of old space in megabytes (MB).
This limit covers only V8's heap. It is not the total memory your operating system shows for the program. It also does not tell you exactly when an app will run out of memory.
The two commands below start Node.js twice, once with a 64 MB old space and once with 256 MB. The -e option runs the code in quotes, which prints the heap limit.
node --max-old-space-size=64 -e "console.log(require('node:v8').getHeapStatistics().heap_size_limit)"node --max-old-space-size=256 -e "console.log(require('node:v8').getHeapStatistics().heap_size_limit)"The test runs both commands and checks that --max-old-space-size=256 gives a bigger heap_size_limit than --max-old-space-size=64. The exact numbers change between computers, so the lesson does not list them.
If a Node.js program runs out of heap, a bigger limit can give it more time. But it does not tell you why memory keeps growing. To find out, use a heap snapshot: a list of every object in the heap at one moment, which browser and Node.js developer tools can take. Also use an allocation profile, which shows which code makes the most objects.
What developers should do
Most web apps never need to think about V8 page sizes or build options. This knowledge helps you read memory measurements:
- A giant array, image data, or a big parsed JSON response may show up in large-object space.
- Many short-lived small objects show up in new space, and are cleaned up quickly.
- A cache that keeps growing shows up in old space.
Each of these is a clue about what to measure next. It is not a rule to follow without measuring.
- Take a heap snapshot to see which objects are still kept, then fix the code that keeps them.
- Measure before you rewrite your data to fit a guess about pages or pointer size.
- Let go of huge arrays or strings once the page no longer needs them.
- In Node.js, treat
--max-old-space-sizeas a way to give the program more room, not as a fix for a leak.
If your question is about how V8 frees memory, read the garbage-collection lessons. If it is about the memory a function call uses, read the stack frames lesson next.
Common misconceptions
- “The JavaScript rules say objects live in new space or old space.” No. Spaces are V8's own way of organizing memory. The language rules never mention them.
- “Pointer compression means Node.js always has a 4 GB heap.” No. The official Node.js builds used here do not turn pointer compression on.
- “An object in large-object space is a leak.” No. It is only too big for a regular page. A leak is memory your app keeps by mistake, and it grows over time.
- “The sandbox and the garbage collector do the same job.” No. The sandbox limits what an attacker can reach. The garbage collector frees objects that nothing uses anymore.
- “The heap limit is the program's total memory.” No. It covers only V8's heap. The program also uses memory outside it.
Each row of this table is one idea. Read across to see what it really means and what it is not.
| Idea | What it really means | What it is not |
|---|---|---|
| JavaScript rules | Objects behave the way the language rules say. | Spaces, pages, and the cage are V8's own choices. The language never mentions them. |
| Pointer compression | Stores an address inside the cage as a 4-byte offset. | It is not the V8 sandbox, and it is not garbage collection. |
| Heap limit | The top size you allow for V8's heap. | It is not the total memory your operating system shows for the program. |
| Large-object space | Where V8 puts objects bigger than 128 KB. | It does not mean the object is a leak or will never be freed. |
Practice exercises
In the bump allocation step-through, which page does avatar land on?
// Page 0 has only 48 bytes left.
// The next object needs 192 bytes after rounding.avatar lands on page 1. Page 0 is full at byte 976, and 976 + 192 is 1168, which is more than the 1024-byte page.
Use the pointer compression model to work out the offset of base + 0x20n.
const base = 0x400000000n;
const address = base + 0x20n;
// Print the offset as 8 hexadecimal digits.const base = 0x400000000n;
const address = base + 0x20n;
const offset = Number(address - base);
console.log("0x" + offset.toString(16).padStart(8, "0"));The offset is 32, which is 20 in hexadecimal. With 8 digits, it prints 0x00000020.
A learner tries to compress exactly base + 4 GB. Is that address inside or outside the cage?
base + 4 GB is outside the cage. The last byte inside is base + 4 GB - 1.
If you raise --max-old-space-size, does the reported heap limit go higher or lower?
Higher. A bigger --max-old-space-size gives a bigger heap_size_limit.
Your dashboard uses more old-space memory after every page change in the app. What should you do before you rewrite how it stores data?
Measure first: take a heap snapshot, record an allocation profile, or repeat the steps that make memory grow, before you change the app.
In this lesson, what does official Node.js 22 print for v8_enable_pointer_compression and v8_enable_sandbox?
Both are 0, which means off: 0 for pointer compression and 0 for the V8 sandbox.
Check your understanding
Question 1 of 8Why does V8 split the heap into spaces?
Choose an answer to see the explanation.
Question 2 of 8What does this bump-allocation snippet print last?
Read the code, then predictconst pageSize = 1024; let page = 0; let nextFree = 976; const size = 192; if (nextFree + size > pageSize) { page += 1; nextFree = 0; } console.log(page);Choose an answer to see the explanation.
Question 3 of 8What does this pointer-compression model print first?
Read the code, then predictconst base = 0x400000000n; const address = base + 0x12345678n; const compressed = Number(address - base); console.log("0x" + compressed.toString(16).padStart(8, "0"));Choose an answer to see the explanation.
Question 4 of 8Which sentence about the official Node.js 22 builds and Chrome is true in this lesson?
Choose an answer to see the explanation.
Question 5 of 8Why does a very big object skip the regular page?
Choose an answer to see the explanation.
Question 6 of 8What does raising
--max-old-space-sizedo tov8.getHeapStatistics().heap_size_limit?Choose an answer to see the explanation.
Question 7 of 8What does the V8 sandbox try to stop?
Choose an answer to see the explanation.
Question 8 of 8Which habit for real apps follows from this lesson?
Choose an answer to see the explanation.
Key takeaways
- V8 splits its heap into spaces, because new objects, long-lived objects, machine code, and very big objects need different care.
- In current V8, a regular page is 256 KB. The biggest regular object is half a page, 128 KB.
- Bump allocation is fast: place the object at
nextFree, then movenextFreeforward. If it does not fit, use a new page. - Pointer compression stores an address as a 4-byte offset inside a 4 GB cage. Chrome uses it. The official Node.js builds used here do not.
- The V8 sandbox is a safety layer. It stops a broken heap from reaching the rest of the program's memory.
Remember the one-liner.
V8 keeps its heap fast and small by sorting objects into spaces, filling pages one object after another, and, in Chrome, storing addresses as short offsets inside a cage.
Up next: Stack frames & calling conventions, where you will see what a function call puts on the stack.