HotShard
Operating systems

Virtual Memory

Every program gets its own private, pretend addresses, and the CPU quietly turns them into real ones.

Chrome, Spotify and your IDE are all running right now, all using the same RAM, and none of them can touch the others' memory. Each one even believes it starts at the same addresses. Here's the trick.

~7 min read

Watch the whole topic · or read it below

Why programs can't share real memory#

TL;DRthe 30-second version
  • Each program gets its own pretend addresses, called virtual addresses.
  • Memory is handed out in 4 KB pieces. A table per program says which real piece backs each pretend one, and the CPU does the translation on every access.
  • The table is a tree, so unused memory costs nothing. Pieces are only handed out the first time they're used.

Think of RAM as a very long row of numbered lockers. Each locker holds one byte. The locker's number is its address. When a program stores a value, it goes into a locker. To read it back, the program asks for that locker number.

Now run many programs on one row of lockers. Two things go wrong. A bug in Chrome writes to the wrong locker, and Spotify's data is gone. And programs are built with fixed addresses in them, so two programs that both use locker 1,000 can't run together. There's only one locker 1,000.

First try: give each program its own block#

The obvious fix: give each program a fixed range. Chrome gets lockers 0 to 999, Spotify 1,000 to 1,999, and the CPU blocks access outside a program's range. Early machines did this. It breaks four ways.

  • Programs can't grow. Chrome needs more, but Spotify's block sits right after Chrome's.
  • Free space breaks into holes: 500 MB free in total, but no 400 MB in one run.
  • Reserved means wasted. A program that reserves 8 GB and uses 2 GB still locks up all 8.
  • No sharing. A hundred programs using one library need a hundred copies.

Small pieces, and a table#

Cut memory into small, equal pieces instead: 4 KB each, which is 4,096 bytes. (KB, GB and TB here count in steps of 1,024, the way memory is measured.) A program that needs memory gets any free pieces, wherever they are.

That fixes growth and holes. But the pieces are scattered: say one at real address 40,000, the next at 900,000, the last at 12,000. Now it stores a 10,000-byte array. Code assumes memory is continuous: element 5,000 is at start + 5,000. That runs off the first piece into someone else's memory.

So the program must never see real addresses. It sees one continuous range of pretend addresses: 0, 1, 2 and up. A table maps each pretend piece to its real piece. Pretend pieces are called pages. Real pieces are called frames. The table is the page table, and every program gets its own. It has one line per page: line 12 holds the frame for page 12.

WhoDoes what
The programUses pretend addresses, as if memory were one continuous block. It never sees the table.
The operating systemBuilds each program's table and keeps a list of free frames.
The CPUTranslates the pretend address into the real one on every single access, in hardware.

Pretend addresses are called virtual addresses: the virtual in virtual memory.

How one address is translated#

A program asks for address 50,000. Pages are 4,096 bytes, laid back to back: page 0 is addresses 0 to 4,095, page 1 is 4,096 to 8,191, and so on. 50,000 ÷ 4,096 = 12, remainder 848. So it's page 12, 848 bytes in. (For now, picture one flat table. The next section shows why the real one is a tree.)

50,000pretend address
÷ 4,096
page 12848 bytes in
table lookup
frame 7,731from the page table
7,731 × 4,096 + 848
31,667,024real address
One address, pretend to real

There's no searching. 4,096 is a power of two, so the CPU just splits the address's bits, and line 12 is found directly, like table[12].

Why the table is a tree#

Predict64-bit CPUs use 48 bits of address today, and Linux gives each program half of that range: about 128 TB of pretend addresses. At 4 KB a page, how many lines would one flat table need?

Hint: 128 TB ÷ 4 KB, then 8 bytes per line.

About 34 billion lines. At 8 bytes each, that's 256 GB of table for every program, almost all of it saying "nothing here".

A phone book for a huge country doesn't list every possible number. A first book lists regions. Most say "nobody here"; the rest point to a smaller book.

The page table works the same way. The top table has one line per big region. An unused region is one line marked empty. A used region points to a smaller table, and so on down, four levels deep. A program pays only for what it uses. The tables themselves are ordinary data, sitting in RAM like everything else.

How does the CPU find its way down? The address spells out the path, like a phone number: country, then city, then line. The CPU cuts the address into chunks, each picking the line at the next level. The last chunk is the position inside the page.

Where does it start? The CPU has a few tiny built-in storage slots, called registers. One of them holds where the current program's top table is. Switching programs means pointing it at another table.

Nothing is loaded until it's touched#

Follow java Hello from the start. Here java is itself a program, a file on disk. The addresses are round, made-up numbers.

  1. The layout is decided before it runs. When the java program was built, its build tools wrote into it: code goes at pretend addresses from 40,960, and running starts at 50,000. Just numbers in a file.
  2. Starting the program loads nothing. The operating system writes a plan of allowed ranges, like "code from 40,960 comes from the java file", and an empty page table.
  3. Empty means one flag. Every line has a present flag, and line 12 says present: no.
  4. The first instruction trips it. The CPU tries 50,000: page 12, not present. It stops the program and calls the operating system. That's a page fault.
  5. The operating system fills it in. The plan says page 12 is code from the file. It takes a free frame, say 7,731, copies one page of the file into it, marks line 12 present at frame 7,731, and lets the program carry on.

Every page gets its frame this way, on first touch, and one page always fits one frame. Memory that doesn't come from a file, like space a program asks for while running, gets a frame filled with zeros. Touch an address outside the plan, and the program is killed with "Segmentation fault".

So 50,000 points nowhere when the program starts. The frame it gets can differ on every run, and even mid-run, if the page is moved to disk and brought back.

What it costs#

With four levels, every memory access means four extra trips to RAM, one per table, before the real one. That would make every program several times slower. It doesn't, because the CPU keeps a small cache of recent translations. That cache is called the TLB, and it's the next article in this series.

What you get for it
  • Safety: a program can only reach frames its own table points to.
  • Simplicity: every program sees the same clean, continuous layout.
  • No waste: reserving memory costs nothing until a page is touched.
  • Sharing: two programs' tables can point at the same frame, so a library sits in RAM once.

The price: translation on every access, and memory for the tables.

If this comes up in an interview#

In interview words: program = process, pretend = virtual, real = physical.

The one-linerEach process gets a private virtual address space. Memory is managed in 4 KB pages. A per-process page table, a four-level tree, maps pages to physical frames, and the CPU translates every access. Frames are attached lazily, on the first touch, through a page fault.
Is virtual memory the same as swap?

No. Virtual memory is the translation. Swap is one thing it enables: moving a page to disk and marking its line not present.

Why does top show a huge VSZ?

VSZ counts addresses reserved in the plan. RSS counts pages that actually have a frame. The first is often far bigger.

Under the hood
  • The real chunk sizes. A 64-bit address uses 48 bits, split 9 + 9 + 9 + 9 + 12. Each 9-bit chunk picks one of 512 lines in a table, and each table is itself one 4 KB page. The last 12 bits are the position, 0 to 4,095. On x86 the register holding the top table is called CR3.
  • Permissions. Each line also has bits for writable, user-accessible and no-execute. Code can be run but not written. Data can be written but not run.
  • Where free frames come from. At boot, the kernel counts RAM, about 4 million frames for 16 GB, and puts every frame it doesn't need on a free list. A program's frames go back on it when the program exits.
  • A real process, measured on arm64 Linux: code at 0xc793…0840, heap at 0xc793…f2a0, a 1 MB malloc at 0xfda7…f010 (the C library serves big requests outside the heap), stack at 0xffff…208c.
  • Random offsets. For security, Linux slides the whole layout by a random amount on each run. That's called ASLR.
  • Bigger machines. Newer CPUs can add a fifth level, for about 64 PB of addresses.
Gotchas
  • VSZ is not memory use. Reserved isn't used.
  • A segfault isn't "out of memory". It's a touch outside the plan, or breaking a permission, like writing to code.
  • Windows' "virtual memory" setting is its swap file, not the translation described here.
References & further reading

Feedback on this topic →