3.4
Virtual Memory, Paging and Swap
How every process gets its own address space, page tables and faults, and why swap is a different thing.
50 min Reading + animations
Tip: tap or hover any dotted-underlined term to see what it means.
By the end of this lesson you will be able to
- Say what virtual memory really is (per-process address translation), and why “using the disk as memory” describes swap, a separate and optional feature.
- Explain virtual and physical addresses, the MMU, pages, frames and page tables, and translate an address by hand.
- Show why page tables are multi-level, and work out what a TLB hit, a TLB miss and a huge page each cost.
- Walk through a page fault step by step, and tell minor faults, major faults and invalid accesses apart.
- Describe how Linux reclaims memory, what swap and
swappinessreally do, why thrashing happens, and how to decide whether a server should have swap. - Explain copy-on-write and
fork, including why a Redis snapshot can double memory use. - Explain
mmapand the page cache, why Kafka and databases rely on them, and what survives a process crash versus a power cut. - Explain overcommit and the OOM killer, read
VSZand resident memory correctly, and size memory limits for containers.
#Where this lesson fits
Lesson 1.2 said in one line that swapping makes everything slow and then the OOM killer ends the process. Lesson 3.1 said each process has its own address space, threads share one, and fork is cheap thanks to copy-on-write, a promise this lesson now explains. Lesson 3.2 listed the TLB among the hidden costs of a context switch; here you see why.
This lesson explains how one physical RAM is shared by hundreds of processes that all believe they own the machine, and what that means for design: why a restart makes a database slow, why Kafka is fast with a small heap, why a container dies while the host has free memory. How a hypervisor adds a second layer of translation under all this belongs to Chapter 4.
#What virtual memory really is
Virtual memory is a technique where each process gets its own private address space, and every address it uses is translated by hardware into a real location in RAM. A program never uses a physical RAM address. It uses virtual addresses, and the CPU and the kernel together decide where each one really lands.
Process A Process B
Virtual address space Virtual address space
| |
v v
A's page tables B's page tables
| |
+---------------+---------------+
v
Physical memory
Two programs can both use address 0x5000 and never collide, because their tables send that number to different places.
One address, two processes, two different places in RAM
Each process has its own map from virtual pages to physical frames. Follow the links: they are the page tables.
Scenario
Both processes use address 0x5000 and never collide.
Two programs run on one machine, and both were built to use address 0x5000 for their data. Without translation they would overwrite each other. This is the problem virtual memory solves.
#Why it is needed
| Benefit | What it gives you | What it means in practice |
|---|---|---|
| Process isolation | A process can only name memory in its own tables | A crash or a wild pointer in one process cannot corrupt another |
| Memory protection | Each page has read, write and execute permissions | Code cannot be overwritten; data cannot be executed |
| Efficient allocation | Continuous virtual memory can sit on scattered frames | Programs get long arrays even when RAM is fragmented |
| Control of physical RAM | The kernel decides what stays in RAM and what leaves | It can reclaim idle memory and enforce per-container limits |
| Demand paging | Frames are given only when a page is first touched | A 1 GB allocation costs nothing until used; programs start quickly |
| Shared memory and mapping | Several tables may point to one frame; files can be mapped | One copy of libc for all processes; mmap; cheap fork |
#Virtual addresses, physical addresses and the MMU
A virtual address is the number a process uses: every pointer you have ever printed is one. A physical address is the actual location on the RAM hardware. Processes never see it.
The MMU (Memory Management Unit) is the part of the CPU that translates one into the other on every single memory access: each instruction fetch, each load, each store. Since it happens billions of times per second, it has to be done in hardware. The kernel’s job is different: it writes the page tables that the MMU reads, and it handles the cases the MMU cannot.
A 64-bit process does not really have 64 bits to use. Common x86-64 CPUs use 48 virtual bits, or 256 TB, of which Linux gives the lower half to user space (128 TB); the upper half is reserved for the kernel. CPUs with five-level paging extend this to 57 bits, 128 PB. So VSZ can be enormous on a machine with little RAM, and that is not a contradiction: an address space is only a range of numbers.
#Pages, frames and the page table
Translating each byte separately would need a table bigger than memory itself, so memory is managed in fixed blocks:
- A page is a fixed-size block of virtual memory.
- A frame is a block of physical memory of the same size.
- The page table says which virtual page lives in which physical frame.
Virtual memory Page table Physical RAM
+--------+ +---------+
| Page 1 | ------------> frame 3 ------------> | Frame 1 |
+--------+ +---------+
| Page 2 | ------------> frame 1 ------------> | Frame 2 |
+--------+ +---------+
| Page 3 | ------------> frame 2 ------------> | Frame 3 |
+--------+ +---------+
The usual size is 4 KB. It is not universal: x86-64 also supports 2 MB and 1 GB pages, Arm servers can use 4, 16 or 64 KB as a base size, and Apple Silicon uses 16 KB. getconf PAGESIZE tells you, and portable code should ask rather than assume.
Each record in a page table is a page table entry (PTE), and it holds more than a frame number:
| Part of a PTE | Meaning | What uses it |
|---|---|---|
| Frame number | Where the page lives | The MMU, for translation |
| Present | Is it in RAM right now? | Off means a page fault on access |
| Writable | May it be written? | Protection, and the copy-on-write trick |
| User | May user code touch it? | Keeps the kernel’s memory safe from processes |
| No-execute | Forbid running code from it | Stops many code-injection attacks |
| Accessed | Set by hardware when used | The kernel uses it to find idle pages |
| Dirty | Set by hardware when written | The kernel knows whether it must save the page before dropping it |
#Translating an address by hand
With 4 KB pages, 4,096 = 2^12, so the lowest 12 bits of an address are the offset inside the page and everything above them is the page number. In hexadecimal, 12 bits is exactly the last three digits, which makes this easy to do by eye.
Take virtual address 0x00403A7C, and suppose the page table maps page 0x403 to frame 0x1F2:
- Split: page number
0x00403, offset0xA7C. - Look up: read entry
0x403of the table. It is an array index, not a search. Frame0x1F2, present. - Keep the offset: a frame and a page have the same size, so the offset does not change.
- Join:
0x1F2followed byA7Cgives physical address0x1F2A7C.
The next byte, 0x00403A7D, is on the same page and needs no new lookup. That is why accessing memory in order is so cheap.
Cutting an address: page number, offset, and the walk
The MMU does not search anything. It cuts the address into fixed fields and uses them as array indexes. All the numbers here are exact.
Scenario
A 32-bit address with 4 KB pages, translated by hand.
0x00403A7C
Page number
0x00403
20 bits
Offset
0xA7C
12 bits
A page is 4 KB, which is 4,096 = 2^12 bytes. So the lowest 12 bits say where inside the page, and the remaining 20 bits say which page. In hexadecimal, 12 bits is exactly the last three digits.
Self-checkQuestion
Pages are 4 KB. A program reads virtual address 0x0012F3A8, and the page table maps page 0x12F to frame 0x8B0. What is the physical address?
#Multi-level page tables
A single flat table is impossibly large. With 48-bit addresses and 4 KB pages there are 2^36 pages, and at 8 bytes per entry that makes 512 GB of page table for each process.
But the address space is mostly empty: a process has a few islands (code, heap, libraries, stack) in a vast sea. So page tables are a tree, and branches over empty regions simply do not exist. On x86-64 with four levels:
47 39 38 30 29 21 20 12 11 0
+------------+------------+------------+------------+------------+
| L4 index | L3 index | L2 index | L1 index | offset |
| 9 bits | 9 bits | 9 bits | 9 bits | 12 bits |
+------------+------------+------------+------------+------------+
- Each table has 512 entries (2^9) of 8 bytes, so a table is exactly one 4 KB page. That neatness is deliberate.
- A CPU register (
CR3on x86) holds the physical address of the top table. Switching process means loading another value there. - A small process needs only a handful of tables, so tens of kilobytes rather than hundreds of gigabytes.
- The price is time: a translation with no help takes four memory reads, plus the read of the data itself.
Page tables do use real memory, though, and it adds up. A 64 GB heap fully touched with 4 KB pages needs about 128 MB of last-level entries per process, and hundreds of processes each mapping a huge shared region can use gigabytes. PostgreSQL with very large shared_buffers and many connections is a known case, and huge pages are the usual fix.
#The TLB: making translation free
Five memory reads for each load would make computers unusable. The TLB (Translation Lookaside Buffer) is a small, fast cache of recent page-to-frame translations inside the CPU.
- TLB hit: the translation comes back in about a clock cycle. This is the overwhelmingly common case.
- TLB miss: a hardware walker reads the tree. Typically tens of nanoseconds, and more when the page-table reads also miss the CPU caches. It is not a fault, and no kernel code runs.
- Page not present: the walker finds the present bit off and raises a page fault. Only then does the kernel run.
TLBs are small. A core typically has a first level of a few dozen entries and a second level of one to a few thousand. What matters is TLB reach: entries × page size.
| Page size | With about 1,500 entries | Fits a working set of |
|---|---|---|
| 4 KB | 1,536 × 4 KB | about 6 MB |
| 2 MB | 1,536 × 2 MB | about 3 GB |
So a program that chases pointers randomly over a 64 GB heap misses the TLB constantly, and may spend a noticeable share of its time walking page tables even though no graph shows it as a problem. Use perf stat -e dTLB-load-misses to measure it. Under a hypervisor the walk is two-dimensional, as guest and host tables multiply; Chapter 4 explains that.
Two costs to remember for design:
- Process switches disturb the TLB. Page 0x403 means different things in different processes. Older hardware had to flush on every switch; with PCID (x86) or ASID (Arm), entries carry an address-space tag and can stay. Switching between threads of one process keeps the same tables, one concrete reason it is cheaper (lesson 3.2).
- TLB shootdowns. Each core has its own TLB. When a multi-threaded process unmaps or changes memory, x86 kernels must interrupt the other cores running it so they drop the stale entry. Frequent
munmapon many cores is a real scalability bottleneck.
One memory load: TLB hit, TLB miss, and when the hardware gives up
The same load through the hardware path. Watch how much extra work a miss costs, and when the kernel is finally involved.
Scenario
The normal case: almost all loads in a healthy program take this path.
The address
Virtual address
0x00403A7C
Page number
0x403
Offset
0xA7C
TLB (a few entries of a small cache)
| Page | Frame | Flags |
|---|---|---|
| 0x403 | 0x1F2 | R W |
| 0x7A1 | 0x09C | R |
The core wants the virtual address 0x00403A7C. The MMU, the translating part of the CPU, takes the page number and asks the TLB first.
#Huge pages
A huge page ends the walk early, at level 2 for 2 MB or level 3 for 1 GB. The main win is TLB reach, and a smaller page-table tree also saves memory.
Explicit huge pages (HugeTLB) | Transparent Huge Pages (THP) | |
|---|---|---|
| How | Reserved in advance (vm.nr_hugepages), requested by the program | The kernel makes them automatically, by khugepaged or at fault time |
| Code changes | Application must ask, usually via a config option | None |
| Swappable | No, always in RAM | Yes, usually split first |
| Risk | Reserved memory is lost to others if unused | Latency spikes from compaction, memory bloat, costly copy-on-write |
| Typical use | Large database buffers (huge_pages in PostgreSQL, Oracle), JVM with -XX:+UseLargePages | General workloads with big heaps, with care |
Trade-offs to weigh:
- Internal fragmentation: touching one byte can commit 2 MB, so sparse data structures waste memory.
- Latency: to build a 2 MB page the kernel may need to compact memory, stalling the faulting thread. Watch
compact_stallin/proc/vmstat. - Copy-on-write: one small write copies 2 MB, which makes the fork problem below worse.
Advice depends on the product and even the version: Redis warns about THP at startup, MongoDB’s recommendation has changed between versions, and many JVM workloads gain from it. Read your product’s documentation, then measure. cat /sys/kernel/mm/transparent_hugepage/enabled shows your setting; madvise (use THP only where the program asks) is a popular middle ground.
#Page faults
A page fault is the hardware exception raised when a translation cannot be completed. Despite the name, it is usually not an error: it is how the kernel gets control at the moment a page is needed. Allocating memory only records a range, and frames are given out on first touch. This is demand paging, and it is why starting a large program does not read the whole file into RAM.
When a process touches a page that is not currently in physical memory:
The kernel checks
The MMU traps into the kernel with the faulting address. The kernel checks its list of valid regions for this process. If nothing covers the address, or the access breaks the permissions, it sends
SIGSEGVand stops here.It finds where the page should come from
Zero-filled memory for a new heap page, the page cache or a file for a file-backed page, or a swap slot for a page paged out earlier.
It loads the page if needed
It gets a free frame, evicting another page if it must, then fills it. If the data is on disk, the thread sleeps while other threads run.
It updates the page table
The entry now holds the frame number, with the present bit on.
The instruction runs again
The CPU restarts the same instruction from the beginning. The program never knows it was interrupted.
#Minor, major and invalid
| Minor fault | Major fault | Invalid access | |
|---|---|---|---|
| What | Page is obtainable without disk | Data must come from storage | No valid mapping, or permission denied |
| Examples | First touch of malloced memory; a file page already in the page cache; a copy-on-write copy | A mapped file not yet cached; a swapped-out page; code pages at a cold start | Null pointer, use after free, write to read-only memory |
| Cost | Around a microsecond | About 100 µs on NVMe, 5–10 ms on a hard disk | Process usually dies |
| Worry? | Normally no, though millions of them cost real CPU | Yes, when sustained on a latency-sensitive path | It is a bug |
Normal RAM access takes about 100 ns, so one major fault on NVMe costs as much as a thousand memory accesses. A request taking a few of these can go from 1 ms to 10 ms with nothing visibly wrong: no CPU spike, no error, and the average hardly moves. Major faults are a classic source of tail latency.
Design consequences:
- Cold starts. After a restart or a new deployment, code and data are not in memory, so the first requests take major faults. That is one reason to warm up an instance before sending it traffic, and to use a slow-start setting at the load balancer (Chapter 25).
- Latency-critical memory.
mlockormlockallpins pages in RAM, and pre-touching at startup (JVM-XX:+AlwaysPreTouch) moves the cost away from requests. - Measure it.
ps -o min_flt,maj_flt -p PID,pidstat -r 1, andperfall show fault rates.
A page fault has three endings: quick fix, disk read, or death
The fault looks the same to the CPU. What differs is what the kernel finds when it checks the address.
Scenario
Allocating does not give you RAM. Touching does. This is demand paging.
malloc of a large block asks the kernel for address space. The kernel just records “this range is valid”. No frames are used yet, and resident memory does not move.
#Swap and memory reclaim
When free memory runs low, the kernel reclaims frames, normally in the background through kswapd. If that cannot keep up, a thread that needs memory must do the reclaim itself (direct reclaim), and your application thread is suddenly stalled in the kernel. What it gives up depends on the kind of page:
| Page type | What the kernel does | Cost |
|---|---|---|
| Clean file page (code, read data) | Drop it; the file has a copy | Free now; a major fault if it is needed again |
| Dirty file page | Write it to its file, then drop it | One disk write |
| Anonymous page (heap, stack) | Write to swap; only possible if swap exists | One disk write now, a major fault later |
To choose which pages, Linux keeps roughly least-recently-used lists, using the accessed bit from the PTE (and since kernel 6.1 it offers the multi-generational LRU as an option).
Swap is disk space used as a backing store for anonymous pages. Paging out and back in is what the notes call swapping. Historically “swapping” meant moving a whole process out; Linux moves individual pages, and people use both words for it. Since SSD and hard disks are orders of magnitude slower than RAM, a touched swapped-out page is painful.
RAM is full: what the kernel gives up first
Reclaim is a price list. Dropping a clean page is free, writing a dirty one costs I/O, and swapping needs a place to swap to.
Scenario
The cheapest thing to reclaim. The disk already has an identical copy.
All four frames are used, and a new page needs a frame. The kernel’s reclaim code looks for what was not used recently, using roughly least-recently-used lists.
#swappiness: not what most people think
vm.swappiness is commonly explained as “swap when free memory drops below this percentage”. It is not. It sets the relative preference between reclaiming anonymous pages and file pages when reclaim happens. The range is 0–200 since kernel 5.8, the default is 60, and 100 treats the two equally. swappiness=0 does not disable swap; the kernel still uses it as a last resort before killing a process. Kafka’s documentation suggests vm.swappiness=1, for example, so that idle heap goes only when there is no other choice.
#Thrashing
Thrashing is when the active working set no longer fits in RAM. Each page brought in pushes out a page needed a moment later, so the machine spends its time moving pages and almost none working. The symptoms: high si and so in vmstat, high I/O wait, collapsed throughput, a sluggish SSH session. Health checks then fail, the load balancer moves traffic to the other nodes, and they may go the same way. More swap does not help, because the problem is speed rather than space. Reduce the working set, add RAM, or shed load. Lesson 3.2 describes the same word for scheduling; the shape of the failure is identical.
#Should a server have swap?
| Choice | Good | Bad | Fits |
|---|---|---|---|
| No swap | Failure is fast and clean: OOM kill and restart. Latency stays predictable | Idle heap pages stay in RAM forever, squeezing the page cache; less room to absorb a spike | Stateless, replicated services behind a load balancer; most containers |
Small swap, low swappiness | Idle pages can leave, leaving more for useful data; time to react to a slow leak | A slow degrade instead of a clean failure, if you are not watching | General-purpose servers; many database hosts |
Compressed swap in RAM (zram) | Far faster than disk; no disk wear | Uses CPU and some RAM | Small VMs, desktops, memory-tight nodes |
| Large swap | Survives big spikes on paper | Can thrash for a long time with the node technically “up” | Rarely a good idea on a latency-sensitive server |
Many cloud images ship with no swap, and Kubernetes historically required it to be off; newer versions can use it in a limited mode, so check yours. Clustered systems such as Elasticsearch ask you to disable swap or lock their memory, because a node paused by swap can be declared dead by its peers. A dead node is easier to handle than a slow one.
Alert on the rate, not the amount. A full swap with zero si is harmless: those pages really are idle. A small but steady si is a real problem. /proc/pressure/memory (PSI) directly reports the percentage of time tasks were stalled waiting for memory, and is the best single signal.
#Copy-on-write and fork
Lesson 3.1 introduced fork, which creates a child with a copy of the parent’s memory. Actually copying would be far too slow. So:
- The kernel copies the page tables, not the frames, and marks every writable page read-only in both.
- Parent and child share all the frames, and reads cost nothing.
- When either one writes, the MMU raises a protection fault. The kernel recognises a copy-on-write (CoW) page, makes a private copy of that one page, repoints the writer, and restarts the instruction.
fork followed by exec, the way every shell starts a program, therefore copies almost nothing. The remaining cost is the page tables, and it grows with process size. Large processes that launch helpers should prefer posix_spawn or vfork.
The system design example: Redis persistence. BGSAVE forks. The child writes a snapshot from its frozen view of memory while the parent keeps serving. It is an elegant design, with two costs that catch people out:
- The fork pauses the server. Redis executes commands on one thread, and that thread is stuck in
forkwhile the page tables are copied. For 24 GB there are about 48 MB of tables to copy; it is on the order of 10 ms per GB on normal hardware and can be much slower on some VMs.INFO statsshowslatest_fork_usec. - Memory can double. Each page updated during the save gets copied. If 30% of a 40 GB data set changes, you need about 52 GB. In the worst case, 80 GB. A machine sized at exactly the data set will hit the OOM killer during a save, typically at the worst moment. THP makes it worse by copying 2 MB per write.
This is why you never size Redis at 100% of RAM, and why Redis asks for vm.overcommit_memory=1: under the default heuristic, a big fork can be refused.
Copy-on-write also explains why pre-fork servers share less memory than you expect. Python and Ruby keep reference counts or garbage-collection marks inside objects, so merely reading an object can write to its page and un-share it. Instagram famously disabled parts of Python’s garbage collector for that reason, and gc.freeze() exists because of it.
Copy-on-write: fork shares everything until someone writes
Page tables are copied, frames are not. This is why fork is cheap, and also where the surprise memory spikes of Redis snapshots come from.
Scenario
The basic mechanism, one page at a time.
Before fork: the parent owns three pages in three frames, normally writable.
#mmap and the page cache
#The page cache
The page cache is RAM the kernel uses to hold file contents. Every normal read and write passes through it, and it uses essentially all idle RAM, because empty RAM does nothing for you. It gives the memory back as soon as programs need it.
- Reads: a hit costs about a microsecond instead of 100 µs or several milliseconds. Sequential reads trigger read-ahead, so they rarely wait.
- Writes: data is copied into the cache and marked dirty, and
writereturns before anything reaches the disk. Flusher threads write it later, by default when it is about 30 seconds old or pastvm.dirty_background_ratio. Pastvm.dirty_ratio, writers are forced to wait, a common cause of write latency spikes. - Durability: only
fsync(orfdatasync) forces it to stable storage. - Ownership: the cache belongs to the kernel. A process crash loses nothing that
writeaccepted, while a power cut or kernel panic loses all dirty pages. The cache also survives a process restart, so a restarted database is warm if it relied on the page cache, and cold if it used only its own buffer pool.
#Why Kafka and databases care
Kafka keeps a small JVM heap, often single-digit gigabytes, and leaves the rest of RAM to the page cache. Messages are appended with ordinary writes, and consumers that keep up read data that is still cached, so no disk is touched. sendfile passes cached pages to the socket without a copy through the application (but TLS in user space loses this shortcut, unless kernel TLS is used). The cache survives a broker restart and stays out of garbage collection. Durability comes from replication rather than an fsync per message. The failure mode: one consumer reading far behind pulls old data in from disk and pushes fresh pages out, slowing everyone. Messaging itself is Chapters 30–32.
Databases differ, and an interviewer may ask why:
| Approach | Example | How it uses memory | Consequence |
|---|---|---|---|
| Own buffer pool, bypassing the kernel | MySQL InnoDB with O_DIRECT | Buffer pool often takes most of RAM (50–80%) | Full control over eviction; cold after restart |
| Own buffer plus the page cache | PostgreSQL | shared_buffers commonly around 25% of RAM; the OS caches the rest | Simple, some double caching; effective_cache_size tells the planner; restart stays partly warm |
| Mapped files | LMDB, Lucene (Elasticsearch, OpenSearch), Prometheus | The kernel decides everything | Simple and fast for reads; heap should be well under total RAM to leave room |
This is the reason “how much RAM does it need?” is really “how large is the working set?”. A database with a 2 TB file and 200 GB of hot data needs about 200 GB of cache. If the working set grows past your cache, performance doesn’t degrade gently; it falls off a cliff as hits become disk reads. Caching in general returns in Chapter 23.
#mmap
mmap asks the kernel to make a file, or plain memory, appear as a region of your address space.
| Use | Flags | What it does |
|---|---|---|
| Read a file as memory | MAP_SHARED or MAP_PRIVATE, read-only | Pages fault in on demand from the page cache |
| Shared memory between processes | MAP_SHARED | Both see the same frames; changes to a file mapping reach the file |
| Private copy of a file | MAP_PRIVATE | Writes get copy-on-write and do not reach the file; this is how programs load their data segments |
| Big allocations | MAP_ANONYMOUS | How malloc gets large blocks, by default from about 128 KB in glibc |
It avoids a copy and a system call per read, and it lets you address files larger than RAM. The costs matter for storage engines:
- Hidden blocking. Any pointer dereference may take a major fault, and you cannot tell which one.
- Errors become signals. A disk error shows up as
SIGBUS, not a return code. - No control of eviction or of write timing. A database must guarantee its log reaches disk before its data pages, and the kernel may write mapped pages at any time.
- Shootdowns. Evicting mapped pages can need TLB shootdowns across cores.
MongoDB removed its mmap engine, MMAPv1, in version 4.2. A well-known 2022 paper, “Are You Sure You Want to Use MMAP in Your Database Management System?”, argues the case against it. Read-mostly systems such as LMDB and Lucene still do very well with it.
The page cache: why reads are fast, writes return early, and Kafka works
Linux keeps file data in spare RAM. Every read and write goes through it, unless you opt out. The scenarios run from a simple read to a power cut.
Scenario
The first read pays for the disk. Everyone after that gets RAM speed.
The read system call goes to the kernel, which first looks for the file’s pages in the page cache.
#Overcommit and the OOM killer
Linux, by default, overcommits: it hands out more virtual memory than exists, because most programs reserve far more than they touch. The setting vm.overcommit_memory picks the policy:
| Mode | Behaviour | When allocation fails |
|---|---|---|
| 0 (default) | Heuristic: refuse only obviously excessive requests | Rarely; the trouble comes later, when pages are touched |
| 1 | Always say yes | Almost never; used for Redis-style big forks |
| 2 | Strict accounting against CommitLimit (swap plus vm.overcommit_ratio% of RAM, unless vm.overcommit_kbytes is set) | Immediately: malloc returns null, which code can handle |
When pages are touched and nothing can be reclaimed, the page fault cannot be satisfied, and it is far too late for malloc to fail. The OOM killer picks a process, largely by memory use (see /proc/PID/oom_score), adjusted by oom_score_adj from -1000 (never) to 1000 (first), and sends SIGKILL. There is no graceful shutdown. dmesg or journalctl -k will show Out of memory: Killed process.
- The victim need not be the culprit. A leaking sidecar can get your database killed, so protect critical services with a low
oom_score_adjand limit everything else. - Malloc success proves nothing on Linux. Code that carefully handles a null return can still be killed later.
- The kernel’s OOM killer can act late, after the machine has been crawling for minutes.
systemd-oomdandearlyoomact earlier, using pressure signals.
Overcommit and the OOM killer: promises the kernel cannot keep
Linux hands out more address space than it has memory, on the bet that most will never be touched. Here is what happens when the bet fails, on a host and in a container.
Scenario
A machine with 16 GB of RAM and no swap. The default heuristic mode.
The request looks plausible, so the kernel says yes. It only records the range. Remember that this costs no frames.
#Reading memory numbers correctly
| Measure | Where you see it | Counts | Common mistake |
|---|---|---|---|
VSZ (virtual size) | ps, VIRT in top, VmSize | Everything mapped, used or not | Treating a large value as a problem. Go, Java and anything using mmap show huge values harmlessly |
| Resident memory | RES in top, VmRSS in /proc/PID/status | Pages in RAM now, including shared ones | Adding it up across processes. Shared pages are counted once per process, so the total can exceed RAM |
Pss (proportional) | /proc/PID/smaps_rollup | Resident, with shared pages split among the sharers | The honest per-process share. Slower to read |
VmSwap | /proc/PID/status | This process’s pages in swap | Ignoring it when resident memory looks small |
available | free -h | What can be used without swapping | Reading free instead |
Example: 8 pre-fork workers each show 2 GB of resident memory. That does not mean 16 GB is in use; if 1.5 GB is shared, the real total is about 1.5 + 8 × 0.5 = 5.5 GB. Pss shows each as about 0.69 GB.
Also, resident memory that stays high after free() isn’t necessarily a leak: allocators often keep freed memory for reuse. A leak is resident memory that keeps climbing under steady load.
#Memory limits in containers
A container isn’t a VM: it uses the host kernel with a cgroup limit (memory.max in cgroup v2). The kernel charges each frame the group uses, including page cache.
- Over the limit: first reclaim within the group, then an OOM kill within the group. Exit code 137 (128 + 9), and
OOMKilledin Kubernetes. Free memory on the host doesn’t help. CPU limits only slow you down, but memory cannot be throttled like that, so memory limits kill.memory.highis a softer limit that slows the group under pressure. - Page cache counts, but is reclaimable. A container reading big files can show usage near the limit and be perfectly healthy. Watch working set (usage minus inactive file cache), as the kubelet does.
docker statsalso excludes inactive cache. freeand/proc/meminfoshow the host’s memory, even inside a container. Read/sys/fs/cgroup/memory.currentandmemory.maxinstead.- Runtimes must know the limit. A modern JVM is container-aware and by default sets max heap at 25% of the limit (
-XX:MaxRAMPercentage). The heap isn’t the process, though: metaspace, thread stacks, direct buffers and JIT code all sit outside it. A 1 GB heap in a 1 GB limit will be killed. Go hasGOMEMLIMIT, and Node has--max-old-space-size. - Kubernetes requests vs limits. A request is used for scheduling and is a reservation in the scheduler’s accounts. A limit is enforced by the kernel. Setting requests equal to limits for memory avoids overcommitting the node, so it’s the common advice for stateful workloads.
#A short note on NUMA
On a multi-socket server, each socket has its own RAM. Memory attached to another socket takes longer to reach, by a factor of perhaps 1.3 to 2. That is NUMA (Non-Uniform Memory Access). Linux by default places a page on the node of the thread that first touches it, so a startup thread that initialises a big buffer can place all of it on one node. The classic failure: a database uses more than one node’s RAM, that node runs out, and the kernel swaps or reclaims even though the other node is free. Use numactl --hardware and numastat to look, and interleave memory (numactl --interleave=all) or pin processes to nodes to fix it. Most small cloud VMs show only one node, but big instances may not.
#What it means for system design
| Decision | What the memory system tells you |
|---|---|
| Sizing a cache or database host | Size for the working set, and keep it in RAM. Falling out of memory is a cliff, not a slope |
| Redis or any forking persistence | Leave headroom for copy-on-write, up to 2× under heavy writes; turn off THP; watch fork time |
| Kafka and log-style stores | Small heap, large page cache, sequential I/O, and watch out for lagging consumers |
| Durability claims | A successful write is not durable. Commit means fsync or replicas |
| Stateless service failure mode | Prefer no swap and an OOM kill plus restart over a node limping along |
| Container limits | Limit = heap + non-heap + some headroom; alert before the limit; expect SIGKILL |
| Deployments and restarts | Expect cold pages. Warm up, and use slow-start at the load balancer |
| Many small processes vs threads | Threads share page tables and keep the TLB warm; processes isolate failures but cost more memory and switching |
#Common production problems
| Symptom | Likely cause | Check | Fix |
|---|---|---|---|
Exit code 137, OOMKilled | Container went over its limit | kubectl describe pod, memory.events | Raise the limit or fix the leak; set runtime limits |
| Process vanishes, nothing in the application log | Host OOM killer | dmesg, journalctl -k | Limit neighbours; oom_score_adj; add RAM |
| Everything is slow, CPU shows I/O wait | Thrashing | vmstat 1 (si, so), PSI | Reduce load, add RAM |
| p99 spikes with no CPU change | Major faults or direct reclaim | pidstat -r, sar -B | mlock hot data, leave free headroom, pre-touch |
| Latency spikes with THP on | Compaction | compact_stall in /proc/vmstat | Set THP to madvise or off for that workload |
| Regular latency spike every save | Fork pause | latest_fork_usec | Use a replica for snapshots; smaller instance; turn off THP |
| Slow for minutes after restart | Cold page cache and buffers | Disk reads high, then falling | Warm up; slow start |
fork: Cannot allocate memory with free RAM | Overcommit policy, or the limit on the number of processes | vm.overcommit_memory, ulimit -u | vm.overcommit_memory=1 for that case, or posix_spawn |
| Slow batch job or backup hurts live traffic | It pushed hot pages out of the cache | cachestat, hit ratio falling | posix_fadvise(DONTNEED), O_DIRECT, separate host |
| Write latency spikes in bursts | Too many dirty pages | Dirty in /proc/meminfo | Lower vm.dirty_* thresholds; faster disk |
#Interview angles
- “What is virtual memory?” Lead with translation and isolation. Mention swap only as an optional extra. This one answer separates candidates.
- “Why is Kafka fast?” Sequential I/O, the page cache and
sendfile, not magic. - “Why does Redis need spare memory?” Fork plus copy-on-write.
- “My container was killed but the node has free memory.” Cgroup limit, not host memory.
- “Is data safe after
write?” Safe from a process crash, not from a power cut, untilfsync. - “Why is a thread switch cheaper than a process switch?” Same page tables, so the TLB stays useful.
#Try it yourself
All of this runs on any Linux machine, including Windows Subsystem for Linux. Read-only commands are safe on production; the drop_caches line is not for production.
# Page size and your memory picture
getconf PAGESIZE
free -h
# A process's virtual layout (code, heap, libraries, stack)
cat /proc/self/maps
# Virtual size vs resident memory for your shell
grep -E 'VmSize|VmRSS|VmSwap' /proc/$$/status
# Top memory users, with minor and major fault counts
ps -eo pid,comm,vsz,rss,min_flt,maj_flt --sort=-rss | head
# Swap activity and memory pressure: si and so should stay near 0
vmstat 1 5
cat /proc/pressure/memory
# Settings from this lesson
sysctl vm.swappiness vm.overcommit_memory vm.dirty_ratio
cat /sys/kernel/mm/transparent_hugepage/enabled
# NUMA layout, if numactl is installed
numactl --hardware
See demand paging. This Python script maps 1 GB and shows resident memory before and after touching it:
import mmap, re, resource
def resident():
text = open('/proc/self/status').read()
return re.search(r'VmRSS:\s+(\d+)', text).group(1) + ' kB'
def minor_faults():
return resource.getrusage(resource.RUSAGE_SELF).ru_minflt
size = 1024 * 1024 * 1024
print('start ', resident(), minor_faults())
m = mmap.mmap(-1, size) # anonymous mapping: 1 GB of address space
print('after mapping ', resident(), minor_faults())
for offset in range(0, size, 4096): # touch one byte in each 4 KB page
m[offset] = 1
print('after touching ', resident(), minor_faults())
Mapping changes almost nothing. Touching grows resident memory by 1 GB, with about 262,000 minor faults (far fewer if THP gives you 2 MB pages, which is a nice way to see that it is on).
See the page cache. The second read is much faster:
dd if=/dev/zero of=$HOME/big.bin bs=1M count=1024 # create a 1 GB file
sync
echo 3 | sudo tee /proc/sys/vm/drop_caches # empty the cache (not on production)
time cat $HOME/big.bin > /dev/null # slow: from disk
time cat $HOME/big.bin > /dev/null # fast: page cache
free -h # buff/cache grew
rm $HOME/big.bin
See a container limit kill. Python allocates a 200 MB string under a 100 MB limit:
docker run --rm -m 100m --memory-swap 100m python:3-alpine \
python -c "x = b'a' * (200 * 1024 * 1024); print('survived')"
echo "exit code: $?" # 137, with no Python traceback
#Summary
Key takeaways
- Virtual memory is per-process address translation, not “disk as RAM”. Swap is optional, and many servers run with none.
- Addresses split into page number and offset. The page table maps pages to frames, and the offset stays the same. The MMU does this on each access.
- Page tables are multi-level because a flat one would be 512 GB per process. An unhelped lookup costs 4 reads plus the data.
- The TLB caches translations. Its reach is entries × page size, which is why huge pages help big heaps. THP can also cause latency spikes.
- A page fault is usually normal. Minor is about a microsecond, major is about 100 µs on NVMe or milliseconds on a hard disk, and invalid gets
SIGSEGV. - Reclaim drops clean pages free, writes dirty pages, and sends anonymous pages to swap. Thrashing happens when the working set doesn’t fit.
swappinessis a preference, not a threshold. - Copy-on-write makes
forkcheap, but a Redis snapshot under heavy writes can need up to double the memory. - The page cache uses all idle RAM.
writesucceeds before the disk does; a process crash keeps the data, a power cut doesn’t. Kafka and many databases depend on it.mmapshares those frames. - Linux overcommits. When the bet fails the OOM killer sends
SIGKILL, and in a container that happens at the limit, with exit code 137, whatever the host has free. - Read
available, notfree.VSZis mostly noise. Resident memory doesn’t add up across processes;Pssdoes.
Answer on your own first, then read the explanation, even when you got it right.
Self-checkQuestion
An engineer says: our servers have no swap, so they are not using virtual memory. What is wrong with this?
Self-checkQuestion
Two processes each print a pointer and both show 0x7ffd0000. One writes to it. What does the other see?
Self-checkQuestion
Pages are 4 KB. How many bits of a virtual address are the offset, and how many pages does a 32-bit address space hold?
Self-checkQuestion
Pages are 4 KB. Virtual address 0x00ABC123 is on page 0xABC, mapped to frame 0x042. What is the physical address?
Self-checkQuestion
Why does x86-64 use a tree of page tables rather than one flat table?
Self-checkQuestion
With four-level paging, a load instruction misses the TLB and the page is present. Ignoring CPU caches, how many memory reads does it take in total, and does the kernel run?
Self-checkQuestion
A service has a 64 GB heap and chases pointers randomly through it. perf shows many TLB misses, and no faults. What should help most?
Self-checkQuestion
Which two statements about a context switch and the TLB are correct? Choose two.
Choose 2
Self-checkQuestion
A multi-threaded proxy allocates and frees large buffers with mmap and munmap per request. It scales badly past 32 cores, and many cores show time spent handling interrupts. What is the likely cause?
Self-checkQuestion
A program mallocs 8 GB and writes to only 1 GB of it. What happens to virtual size and resident memory?
Self-checkQuestion
Which of these is a major page fault?
Self-checkQuestion
Which statement about page fault cost is right, for an NVMe machine?
Self-checkQuestion
A service's p99 latency jumps from 5 ms to 40 ms for a few minutes after each deployment, with low CPU and no errors. Which two explanations and fixes fit? Choose two.
Choose 2
Self-checkQuestion
Under memory pressure, which page can Linux reclaim most cheaply?
Self-checkQuestion
A server with no swap and plenty of idle heap keeps evicting its page cache, and file reads are slow. Why would a small swap possibly help?
Self-checkQuestion
An engineer sets vm.swappiness=10, expecting swap to start only when 10% of RAM is free. What does the setting really do?
Self-checkQuestion
vmstat shows si and so in the thousands every second, I/O wait is high and throughput has collapsed. Which response addresses the cause?
Self-checkQuestion
Which two metrics best indicate real memory trouble on a Linux server? Choose two.
Choose 2
Self-checkQuestion
A 20 GB process forks, and the child immediately calls exec. How much memory is copied?
Self-checkQuestion
A 40 GB Redis on a 64 GB machine is killed by the OOM killer during a background save, under heavy writes. What is the mechanism?
Self-checkQuestion
Why does transparent huge pages make Redis snapshots more expensive?
Self-checkQuestion
A pre-fork Python server has 8 workers sharing a large read-only data set. Over time their resident memory rises, although no one writes to that data. Why?
Self-checkQuestion
An application calls write, gets success, and then the process is killed with kill -9. Soon after that the machine loses power, before any writeback. Which statement is right?
Self-checkQuestion
free -h shows total 62Gi, used 18Gi, free 312Mi, buff/cache 44Gi, available 43Gi, and swap 0. A colleague wants to add RAM urgently. What do you say?
Self-checkQuestion
Which two statements explain why Kafka keeps a small JVM heap and leaves most RAM to the kernel? Choose two.
Choose 2
Self-checkQuestion
A Kafka broker's latency is fine until someone replays a topic from a week ago. Then all clients get slower. What is happening?
Self-checkQuestion
Why do many large relational databases avoid using mmap for their data files?
Self-checkQuestion
On Linux with default settings, malloc for 12 GB succeeds on a 16 GB machine where 6 GB is already in use. Later the process is killed. Which statement is right?
Self-checkQuestion
A container with a 512 MB limit is killed with exit code 137, while the host has 40 GB free. Which two statements are true? Choose two.
Choose 2
Self-checkQuestion
A Java service in a 2 GB container has -Xmx2g and keeps getting OOMKilled, although its heap graphs look healthy. What is the problem?