Virtual memory doesn’t actually make your computer have more RAM; it tricks programs into thinking they do.
Let’s see it in action. Imagine a simple C program that allocates a huge chunk of memory, way more than your physical RAM.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // For sbrk
int main() {
// Request a large amount of memory, e.g., 10GB
// Note: This won't immediately allocate physical RAM.
size_t size = 10ULL * 1024 * 1024 * 1024; // 10 GiB
char *mem = malloc(size);
if (mem == NULL) {
perror("malloc failed");
return 1;
}
printf("Successfully allocated %zu bytes of virtual memory.\n", size);
// Try to write to a small part of it to force a page fault
mem[0] = 'A';
printf("Wrote to memory address %p. Value: %c\n", (void*)&mem[0], mem[0]);
// Keep the process alive for a bit to inspect
printf("Sleeping for 60 seconds. PID: %d\n", getpid());
sleep(60);
free(mem); // This will also be a virtual deallocation
printf("Memory freed.\n");
return 0;
}
Compile and run this: gcc -o virtual_mem_test virtual_mem_test.c && ./virtual_mem_test. You’ll see it print "Successfully allocated…" and then "Sleeping…". The key is that malloc didn’t fail, even if you only have 8GB of RAM. The system promised that memory, but it hasn’t actually put any of it into physical RAM yet.
The problem virtual memory solves is one of scarcity and isolation. If every program needed contiguous physical RAM and had direct access, you’d quickly run out of memory for anything complex. Plus, one buggy program could easily corrupt another’s memory or the kernel’s. Virtual memory provides an abstraction layer.
Internally, it’s a dance between the CPU’s Memory Management Unit (MMU), the operating system’s kernel, and the RAM (and sometimes swap space on disk). When a program accesses memory, the MMU translates the virtual address the program uses into a physical address in RAM. This translation is done using page tables, managed by the kernel. Each process has its own set of page tables.
A "page" is a fixed-size block of memory, typically 4KB on x86. The MMU looks up the virtual page number in the process’s page table. If the page table entry indicates the page is present in RAM and maps to a physical frame, the MMU provides the physical address, and the access succeeds. If the entry shows the page is not in RAM (it might be swapped out to disk, or it might be unallocated like in our malloc example), the MMU triggers a page fault. The CPU stops, and control transfers to the kernel. The kernel’s page fault handler then figures out what to do: load the page from swap, allocate a new page of RAM, or signal an error (like a segmentation fault if the access was invalid).
The magic of that malloc call succeeding is that malloc (or the underlying sbrk system call) doesn’t immediately ask the kernel for physical RAM. It just updates the process’s virtual address space to reserve that range of addresses. The actual physical RAM allocation, and the creation of page table entries mapping virtual pages to physical frames, only happens on demand, when the program first tries to read from or write to a specific virtual address. This on-demand loading is called demand paging.
The one thing most people don’t realize is that the kernel doesn’t just track which physical pages are used by which process. It also tracks the state of each page. A page might be "clean" (meaning it hasn’t been modified since it was loaded from disk or allocated), or "dirty" (meaning it has been modified). When the kernel needs to free up physical RAM (e.g., to load a new page), it prioritizes freeing clean pages because they can simply be discarded – their contents are already safely stored on disk (in the original executable file or in swap). Dirty pages, however, must be written back to disk before they can be reclaimed, which is a much slower operation.
The next concept to grapple with is how the kernel decides which page to evict when it needs more RAM: page replacement algorithms.