OS Concepts
Intermediate 12 min read

Memory management & virtual memory

In one line

Virtual memory gives every process its own private address space, which the OS and hardware map onto physical RAM page by page, loading pages from disk only when needed.

What it is

Programs do not use physical RAM addresses directly. Each process sees its own virtual address space, divided into fixed-size pages (usually 4 KB). Physical RAM is divided into frames of the same size. A page table per process maps each virtual page to a physical frame, and the memory management unit (MMU) in the CPU does this translation on every memory access.

This gives isolation (one process cannot see another's memory), lets programs use more memory than physically exists (rarely used pages live on disk in swap), and allows sharing (for example, one copy of a shared library mapped into many processes). A page fault happens when a program touches a page that is not currently in RAM; the OS loads it and, if RAM is full, first evicts another page using a replacement policy such as LRU.

Why interviewers ask

Memory questions connect theory to everyday production issues: out-of-memory kills, slow servers that are swapping, memory leaks and why a cache can make performance much better or much worse. Interviewers check you understand address translation, page faults, the TLB, thrashing and replacement algorithms, and can trace one by hand.

How it works

  1. 1A virtual address is split into a page number and an offset. With 4 KB pages (4096 = 2^12 bytes), the lowest 12 bits are the offset and the rest is the page number.
  2. 2The MMU first checks the TLB (translation lookaside buffer), a small cache of recent page-to-frame translations. A TLB hit gives the frame immediately.
  3. 3On a TLB miss, the hardware or OS walks the page table. If the page table entry is valid, the frame number is found and cached in the TLB.
  4. 4If the entry is not present, a page fault occurs: the OS finds the page on disk (swap or the program's file), picks a free frame or evicts a victim page (writing it to disk first if it was modified, the 'dirty' bit), loads the page, updates the table, and restarts the instruction.
  5. 5Replacement policies: FIFO evicts the oldest loaded page (and can suffer Belady's anomaly, where more frames cause more faults); LRU evicts the least recently used page; Optimal evicts the page used furthest in the future (not implementable, used as a benchmark). Real systems approximate LRU, for example with the clock algorithm.
  6. 6If processes' working sets do not fit in RAM, the system spends most of its time moving pages to and from disk: this is thrashing, and throughput collapses.

Worked example · Address translation, then LRU with 3 frames

  1. Translation with 4 KB pages: virtual address 0x3A7F. The low 12 bits (three hex digits) 0xA7F are the offset; the remaining 0x3 is page 3.
  2. The page table says page 3 is in frame 7, so the physical address is frame 7 with offset 0xA7F, which is 0x7A7F.
  3. LRU with 3 frames and references 7, 0, 1, 2, 0, 3, 0, 4.
  4. 7, 0, 1: three faults, frames hold [7, 0, 1].
  5. 2: fault. The least recently used is 7, so evict it. Frames [2, 0, 1].
  6. 0: hit. Now 0 is most recently used.
  7. 3: fault. Last uses: 1 at step 3, 2 at step 4, 0 at step 5. Evict 1. Frames [2, 0, 3].
  8. 0: hit.
  9. 4: fault. Last uses: 2 at step 4, 3 at step 6, 0 at step 7. Evict 2. Frames [4, 0, 3].
  10. Total: 6 page faults and 2 hits for 8 references.

Picture it

Virtual memory is like a library with a small reading desk. Every reader thinks they have the whole library to themselves (their own catalogue numbers), but only a few books fit on the desk at once. The librarian (OS) swaps books between the shelves (disk) and the desk (RAM), putting back the one you have not opened for the longest time.

Reference code

from collections import OrderedDict

def lru_page_faults(refs, frames):
    """Count page faults with LRU replacement and `frames` physical frames."""
    memory = OrderedDict()           # pages, least recently used first
    faults = 0
    for page in refs:
        if page in memory:
            memory.move_to_end(page)     # hit: now most recently used
        else:
            faults += 1                  # page fault: load from disk
            if len(memory) == frames:
                memory.popitem(last=False)   # evict least recently used
            memory[page] = True
    return faults

print(lru_page_faults([7, 0, 1, 2, 0, 3, 0, 4], 3))  # 6

An OrderedDict keeps pages in order of last use: a hit moves the page to the end, and eviction removes from the front. Each reference is O(1). For the reference string in the example it reports 6 faults, matching the hand trace.

Key patterns

Trace page replacement by hand

When: FIFO, LRU or Optimal questions

How: Write frames as a row per reference, mark faults, and for LRU track when each page was last used.

Split the address

When: Address translation questions

How: Offset bits = log2(page size); page number = remaining high bits; physical = frame number joined with the same offset.

Working set must fit

When: Diagnosing slow servers

How: If active memory exceeds RAM, you will thrash; reduce memory use, add RAM, or run fewer processes.

Bounded caches

When: Application-level caching

How: Give caches a maximum size and an eviction policy such as LRU so they cannot grow until the OS starts swapping or killing.

Real-world use

Linux OOM killer

When Linux runs out of memory and cannot reclaim enough, the out-of-memory killer picks a process (based on an oom_score) and kills it. In Kubernetes, a container that exceeds its memory limit is killed with reason OOMKilled.

Redis and databases

Redis keeps its data in RAM and its documentation recommends avoiding swap, because a swapped-out page turns a sub-millisecond lookup into a disk read. PostgreSQL relies on both its own shared buffers and the OS page cache for data files.

Huge pages

Databases and JVMs can use 2 MB huge pages on x86-64 instead of 4 KB pages, so each TLB entry covers far more memory and TLB misses drop for large heaps.

fork and copy-on-write

Redis creates RDB snapshots by forking: parent and child share pages until the parent writes, so a snapshot of a large dataset does not need double the memory unless many pages change during the snapshot.

In a project

A Java API server on an 8 GB VM becomes extremely slow every afternoon, with p99 latency jumping from 100 ms to several seconds.

  1. Check memory metrics with tools like free, vmstat and the node dashboard; find swap usage climbing and high major page faults per second, a sign of thrashing.
  2. Discover the JVM heap is set to 6 GB while a sidecar process and the OS page cache also need memory, so the working set does not fit.
  3. Reduce the heap to fit comfortably, fix a cache that grew without bound by giving it a size limit with LRU eviction, and lower swappiness or disable swap for this latency-sensitive service.
  4. Set container memory limits and alerts on memory usage and major faults.
  5. Load test to confirm memory stays flat over several hours.

Impact: Major page faults drop to near zero, p99 latency stays around 100 ms all day, and memory problems now trigger an alert before users notice.

Trade-offs

Use it when

  • Virtual memory: always, in general-purpose OSes, for isolation, sharing and flexible allocation
  • Swap: as a safety net for rarely used memory on general machines
  • Huge pages: large, long-lived memory regions like database buffers or big JVM heaps

Avoid it when

  • Relying on swap for latency-sensitive services such as Redis or trading systems
  • Unbounded in-memory caches: they grow until swapping or an OOM kill
  • FIFO page replacement when access patterns have strong reuse; it ignores recency and can show Belady's anomaly

Cost: A TLB hit adds almost no time; a page table walk costs a few extra memory accesses; a major page fault from disk costs microseconds on SSD to milliseconds on spinning disk, thousands of times slower than RAM.

Common mistakes

  • Saying virtual memory just means 'using disk as RAM'. Its main jobs are isolation and address translation; swapping is only one feature.
  • Forgetting the TLB when explaining why translation is fast.
  • Mixing up internal fragmentation (wasted space inside an allocated block or page) and external fragmentation (free memory split into pieces too small to use).
  • Claiming LRU is what operating systems implement exactly; they use approximations like the clock algorithm because exact LRU is too costly.
  • Thinking adding more frames always reduces faults: with FIFO, Belady's anomaly can make it worse.

Interview questions

Quick revision

  • Virtual address = page number + offset; 4 KB page = 12 offset bits.
  • Page table maps page to frame; the MMU translates; the TLB caches translations.
  • Page fault: page not in RAM, OS loads it, evicting a victim if needed.
  • Dirty page must be written back before eviction.
  • FIFO (Belady's anomaly), LRU (recency), Optimal (benchmark only), clock (LRU approximation).
  • Thrashing: working set larger than RAM.
  • Internal fragmentation: waste inside blocks; external: scattered free gaps.
  • OOM killer ends a process when memory runs out.

Check yourself

1. With 4 KB pages, how many bits of a virtual address are the page offset?

2. Using LRU with 3 frames, how many page faults does the reference string 7, 0, 1, 2, 0, 3, 0, 4 cause?

3. What is the TLB?

4. A server's CPU is mostly idle but disk activity is very high and everything is slow, with swap usage growing. What is the most likely cause?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free