Virtual memory gives every process the illusion of its own private, contiguous memory starting at address 0. The OS and the CPU's memory management unit translate these virtual addresses to real physical locations behind the scenes. It is what keeps processes isolated, lets programs use more memory than you have RAM, and explains why process context switches are expensive.
The problems it solves#
Without virtual memory, every process would address physical RAM directly:
- Isolation: Process A could read or overwrite Process B's memory. One buggy program could crash everything, or a browser could read your banking app's memory.
- Size: 16 GB of RAM can't hold 10 processes that each want 4 GB.
- Fragmentation: if A uses 0–2 GB, B uses 2–5 GB and A exits, you get a 2 GB hole. A process that needs 3 GB contiguous doesn't fit in it, even though plenty is free overall.
The core idea#
Each process uses the same small virtual addresses, but the OS maps them to different physical addresses:
| Virtual address | Holds | Process A → physical | Process B → physical |
|---|---|---|---|
0x0000 |
code | 0x5000 |
0x3000 |
0x1000 |
heap | 0x8000 |
0x6000 |
0x2000 |
stack | 0x2000 |
0x9000 |
Both processes think their code starts at 0x0000, but those addresses map to different physical locations. A can't reach B's memory because the translation hardware has no mapping that leads there.
Pages, frames and the page table#
Translation works in fixed-size chunks rather than byte by byte.
- Virtual memory is split into fixed-size pages (typically 4 KB).
- Physical RAM is split into same-sized frames.
- Each process has a page table that maps its pages to frames, with a valid bit and permissions.
Process A's page table might look like this:
| Virtual page | Physical frame | Valid? | Permissions | Holds |
|---|---|---|---|---|
| 0 | 5 | Yes | Read+Exec | code |
| 1 | 8 | Yes | Read+Write | heap |
| 2 | 2 | Yes | Read+Write | stack |
| 3 | -- | No | Read+Write | on disk |
| 4 | 11 | Yes | Read+Write |
Every memory access is translated in three steps:
- Split the virtual address into a page number and an offset.
- Look up the page number in the page table to get a frame number.
- Combine frame and offset into a physical address, and read RAM.
Page faults#
Page 3 isn't in RAM; it has been swapped to disk. The process doesn't know. When it touches page 3:
- The CPU tries to access page 3.
- Hardware checks the page table: Valid = No.
- Hardware raises a page fault.
- The OS takes over:
- Find a free frame.
- None free? Pick a victim to evict (LRU or an approximation).
- Victim modified (dirty)? Write it to disk first.
- Read page 3 from disk into the frame.
- Update the page table: page 3 → frame 7, Valid = Yes.
- The instruction that faulted restarts.
- The access now succeeds.
Disk is orders of magnitude slower than RAM, so frequent faults hurt. When the system spends most of its time swapping pages in and out, it is thrashing. Adding RAM often speeds up a slow machine for exactly this reason: fewer page faults.
The TLB: caching translations#
The page table lives in RAM, so a naive translation would cost an extra memory access on every access. The CPU avoids that with the TLB (Translation Lookaside Buffer), a tiny, very fast cache of recent page → frame translations.
Say the program accesses virtual address 0x1042, which is page 1, offset 0x042. The TLB currently holds:
| Page | Frame |
|---|---|
| 0 | 5 |
| 1 | 8 (hit: no page-table lookup needed) |
| 4 | 11 |
- Hit: translate immediately.
- Miss: walk the page table in RAM, then cache the result.
TLB hit rates are usually very high (often around 99%) because of locality: programs keep touching the same pages.
This is why process switches are expensive. Process B has a different page table, so the TLB entries cached for Process A are useless (the OS flushes them, or they're tagged and simply don't match). B starts with a cold TLB and pays for misses until it warms up. Threads in one process share the page table, so a thread switch keeps the TLB warm.
How it solves the three problems#
Pages and per-process page tables address each of the three problems from the start of this lesson.
| Problem | How virtual memory fixes it |
|---|---|
| Isolation | Each process has its own page table; the same virtual address maps to different frames, and hardware enforces it. |
| Size | Only the working set (pages in active use) must be in RAM. A process with 4 GB of virtual space might have 500 MB resident; the rest waits on disk. |
| Fragmentation | Pages are contiguous in virtual space, but frames can be anywhere. A 3 GB request becomes contiguous virtual pages backed by scattered frames. |
For example, Process C's pages are contiguous in virtual memory but scattered in RAM, and the process never knows:
| Virtual page (contiguous) | Physical frame (scattered) |
|---|---|
| 0 | 2 |
| 1 | 9 |
| 2 | 4 |
| 3 | 15 |
Demand paging#
When a program launches, the OS doesn't load all of it. It sets up the page table with entries marked invalid, and page faults bring pages in only as the program touches them. Pages you never use are never loaded, which is why a 500 MB application might occupy only 50 MB of RAM.
Key takeaways#
- Each process sees a private, contiguous address space; the MMU translates virtual pages to physical frames through a per-process page table.
- Isolation, running more than fits in RAM, and avoiding fragmentation are the three problems it solves.
- A page fault loads a missing page from disk and restarts the instruction; too many faults means thrashing.
- The TLB caches translations; switching processes leaves it cold, which is a big part of process-switch cost.
- Demand paging loads only the pages a program actually touches.