Quick Answer

Virtual memory gives every process its own private address space that gets translated to real physical RAM addresses behind the scenes, in fixed-size chunks called pages. A page table holds that mapping, a small hardware cache called the TLB speeds up the lookup, and when a page a process needs isn't actually in RAM, the CPU triggers a page fault so the OS can load it from disk.

Why Virtual Memory Exists

Without virtual memory, every running program would need to know the real, physical RAM addresses it's allowed to touch, and the OS would need to make sure no two programs ever pick the same address at the same time. That's fragile and doesn't scale - it's also why old single-tasking systems really could crash the whole machine if one program wrote to the wrong address.

Virtual memory solves this by giving every process its own private address space, starting at address 0, as if it owned the entire machine. The CPU's memory management unit (MMU) translates every one of those virtual addresses into a real physical address at the moment of access, using a mapping the OS controls. Two processes can both think they're using address 0x1000 and never collide, because the OS maps each process's 0x1000 to a different real physical location, or to none at all if that process shouldn't have access - which is also where memory isolation between processes comes from.

Pages and Frames: The Actual Unit of Translation

Virtual memory doesn't translate addresses one byte at a time - that would need an impossibly large mapping table. Instead, both virtual and physical memory are divided into fixed-size chunks. A chunk of virtual memory is a page; the matching chunk of physical memory is a frame. Most systems use 4 KB pages by default (x86's standard page size, though huge pages of 2 MB or 1 GB exist for workloads that benefit from fewer, bigger mappings).

A virtual address splits into two parts: a virtual page number (which page) and an offset within that page (which byte). The MMU only needs to translate the page number - the offset passes through unchanged, since a page and its matching frame are the same size.

PAGE_SIZE = 4096  # 4 KB

vpn = virtual_addr // PAGE_SIZE
offset = virtual_addr % PAGE_SIZE
# physical_addr = (page_table[vpn] * PAGE_SIZE) + offset

The Page Table: Where the Mapping Actually Lives

The page table is the OS-maintained data structure mapping each process's virtual page numbers to physical frame numbers. Here's a real simulation - 3 physical frames available, a program touching 6 addresses across 4 distinct virtual pages, run in Python:

virtual=0x0000 -> vpn=0  offset=0x000 | pfn=2 -> physical=0x02000
virtual=0x1000 -> vpn=1  offset=0x000 | pfn=1 -> physical=0x01000
virtual=0x0004 -> vpn=0  offset=0x004 | pfn=2 -> physical=0x02004
virtual=0x2000 -> vpn=2  offset=0x000 | pfn=0 -> physical=0x00000

Notice the second access to vpn 0 (at virtual address 0x0004) lands on the same physical frame (2) as the first access, just 4 bytes further in - exactly what the offset arithmetic guarantees. Every address inside one page always maps to the same frame.

The TLB: A Cache for Translations

Walking a full page table on every single memory access would be slow, so CPUs cache recent virtual-to-physical translations in a small, very fast piece of hardware called the Translation Lookaside Buffer (TLB). A TLB hit skips the page table walk entirely.

Running the same 6-address sequence with a 4-entry TLB tracked alongside the page table produced this real hit/miss trace:

virtual=0x0000 -> vpn=0 [TLB MISS -> page table (PAGE FAULT)]
virtual=0x1000 -> vpn=1 [TLB MISS -> page table (PAGE FAULT)]
virtual=0x0004 -> vpn=0 [TLB HIT]
virtual=0x2000 -> vpn=2 [TLB MISS -> page table (PAGE FAULT)]

TLB hits: 1, TLB misses: 3 (so far)

The third access is the interesting one: same page (vpn 0) as the very first access, so the translation is still sitting in the TLB and the page table never gets touched at all.

Page Faults and Eviction Under Memory Pressure

With only 3 physical frames and a program touching 4+ distinct pages, something has to give. Extending the access sequence with a 4th and 5th distinct page, and a repeat access to an early page, produced this real trace (LRU - least recently used - eviction policy):

virtual=0x2000 -> vpn=2 [PAGE FAULT] (fills last free frame)
virtual=0x3000 -> vpn=3 [PAGE FAULT]
    [evict] vpn=1 written back to disk, frame 1 reused, TLB entry invalidated
virtual=0x1000 -> vpn=1 [PAGE FAULT]  (vpn 1 was evicted - has to fault back in)

Total page faults: 5
TLB hits: 1, TLB misses: 5, TLB hit rate: 16.7%

This is demand paging in miniature: a page only gets loaded into a physical frame when something actually touches it, and when frames run out, the OS picks a victim (here, the least recently used page) to write back out and reclaim its frame for the new page.

A Real Bug This Simulation Hit (And Why It Matters)

Building this simulation surfaced two genuine bugs worth walking through, because they're not simulation artifacts - they're the exact class of bug real MMU designs have to guard against.

Bug 1 - LRU tracking that ignored TLB hits. The first version of the simulation only updated a page's "last used" timestamp when the page table was actually walked - meaning a page repeatedly hit in the TLB looked "cold" to the eviction logic, even though it was being used constantly. That produced a wrong eviction choice. The fix: real hardware sets a page's "accessed" bit on every reference, TLB hit or not, so the simulation's LRU tracking has to update on every access too, not just on a full page-table walk.

Bug 2 - a stale TLB entry after eviction. After fixing bug 1, a second, more serious bug appeared: when the OS evicted vpn 1 from the page table and reused its physical frame for a different page, the TLB still had vpn 1's old mapping cached. The next access to vpn 1 got a TLB "hit" - and confidently returned the wrong physical frame, one that now belonged to a completely different page.

This is exactly why real CPUs perform what's called a TLB shootdown: whenever the OS evicts a page (or changes its mapping, or a process exits), it must explicitly invalidate that page's TLB entry, on every core that might have cached it, before the frame gets reused. Skip this step and you get memory corruption - one process silently reading or writing another page's data through a translation that's already lying to it. Fixing this in the simulation (and re-running it) gave the correct final trace shown in the previous sections.

Why This Matters Day to Day

You don't manage page tables directly as an application developer, but the consequences of this machinery show up constantly:

  • Thrashing: if a process's working set (the pages it actively needs) doesn't fit in the physical RAM it's been given, it spends most of its time faulting pages in and out instead of doing real work - this is why a container or VM sized too small for its workload can suddenly become far slower than a linear resource cut would predict.
  • Swap / page file: what this post calls "disk" in the simulation is exactly what your OS calls swap space (Linux) or the page file (Windows) - the backing store for pages that don't currently fit in RAM.
  • Why memory isolation is a hardware guarantee, not just an OS convention: a process literally cannot address another process's physical memory, because its virtual addresses only ever get translated through its own page table.

Frequently Asked Questions

What's the actual difference between virtual memory and RAM? RAM is the real, physical hardware with a fixed size. Virtual memory is an address space abstraction each process gets, which can be larger than physical RAM and gets transparently mapped (and partly swapped to disk) by the OS and MMU.
Why is the page size usually 4 KB? It's a tradeoff: smaller pages waste less RAM on partially-used pages but need bigger page tables and more frequent faults; 4 KB has been x86's standard balance for decades, though modern systems also support 'huge pages' (2 MB or 1 GB) for workloads with large, predictable memory access patterns.
What is thrashing? Thrashing is when a system spends more time paging data in and out of RAM than actually running the program, because the process's active working set doesn't fit in the RAM it's been allocated.
Is a TLB a real, physical part of the CPU, or just a simplification for teaching? It's real hardware - every modern CPU core has a physical TLB, and TLB misses have a measurable performance cost, which is part of why huge pages and TLB-aware software exist.
Is virtual memory the same thing as swap space? No - virtual memory is the whole addressing abstraction; swap space (or the page file) is just the disk-backed overflow area the OS uses when physical RAM can't hold every page a process needs right now.