Memory Management, Virtual Memory & Page Cache
Chapter 4: Memory Management, Virtual Memory & Page Cache
Section titled “Chapter 4: Memory Management, Virtual Memory & Page Cache”Learning Objectives
Section titled “Learning Objectives”- Understand virtual memory and why every process gets its own address space
- Understand the page table hierarchy and TLB
- Understand the Linux page cache and how it dramatically speeds up I/O
- Understand the slab allocator for kernel object allocation
- Know how to diagnose memory issues: leaks, pressure, OOM kills
Prerequisites
Section titled “Prerequisites”What is this? (Beginner On-Ramp)
Section titled “What is this? (Beginner On-Ramp)”Memory management is how Linux handles RAM. Instead of letting programs fight over physical memory chips, Linux gives every program a ‘virtual’ memory space—an illusion that it owns the whole computer. The kernel then secretly maps this virtual space to actual physical RAM, swapping things out to disk if it runs out.
4.1 The Memory Problem
Section titled “4.1 The Memory Problem”Without memory management, processes would:
- Directly share physical RAM → any process could read another’s passwords
- Be limited to available physical RAM → no more than RAM can be used
- Need to coordinate addresses → impossible with multiple programs
Virtual memory solves all three problems.
4.2 Virtual Memory: Every Process Gets Its Own World
Section titled “4.2 Virtual Memory: Every Process Gets Its Own World” Virtual Memory Architecture ───────────────────────────
Process A (PID=100) Process B (PID=200) sees this address space: sees this address space: ┌────────────────────┐ ┌────────────────────┐ │ 0xFFFF... │ │ 0xFFFF... │ │ Kernel space │ │ Kernel space │ │ (shared, hidden) │ │ (shared, hidden) │ │────────────────────│ │────────────────────│ │ 0x7FFF... │ │ 0x7FFF... │ │ Stack (grows ↓) │ │ Stack (grows ↓) │ │ ↓ │ │ ↓ │ │ ... │ │ ... │ │ ↑ │ │ ↑ │ │ Heap (grows ↑) │ │ Heap (grows ↑) │ │ Libraries (mmap) │ │ Libraries (mmap) │ │ .data/.bss │ │ .data/.bss │ │ .text (code) │ │ .text (code) │ │ 0x0000... │ │ 0x0000... │ └────────────────────┘ └────────────────────┘ │ │ │ Page tables translate │ │ virtual → physical │ ▼ ▼ ┌────────────────────────────────────────────────┐ │ PHYSICAL RAM │ │ │ │ Frame 0x1000 Frame 0x2000 Frame 0x3000 │ │ [A's stack ] [B's heap ] [Shared libc] │ └────────────────────────────────────────────────┘Each process sees addresses from 0 to 2^48 (on x86_64). These are virtual addresses — they don’t correspond to physical memory locations. The CPU’s Memory Management Unit (MMU) translates them.
4.3 Page Tables: The Translation Mechanism
Section titled “4.3 Page Tables: The Translation Mechanism”Memory is divided into fixed-size chunks called pages (4KB by default on x86_64).
Virtual Address Translation (x86_64, 4-level paging) ───────────────────────────────────────────────────── Virtual Address: 64 bits ┌──────┬──────┬──────┬──────┬──────┬────────────────┐ │ Sign │ PML4│ PDP │ PD │ PT │ Page Offset │ │ 16 │ 9 │ 9 │ 9 │ 9 │ 12 │ └──────┴──────┴──────┴──────┴──────┴────────────────┘
Translation walk (takes 4 memory accesses): CR3 register → PML4 table │ use bits [47:39] ▼ PDPT entry │ use bits [38:30] ▼ PD entry │ use bits [29:21] ▼ PT entry │ use bits [20:12] ▼ Physical page + offset [11:0] = Physical address!
Page Table Entry (PTE) contains: • Physical frame address • Present bit (is page in RAM?) • Read/Write bit • User/Supervisor bit (user/kernel access) • Dirty bit (has page been written?) • Accessed bit (has page been read?) • No-Execute (NX) bit (prevent code execution)TLB: The Translation Cache
Section titled “TLB: The Translation Cache”4 memory accesses for every virtual-to-physical translation would be catastrophically slow. The TLB (Translation Lookaside Buffer) is a hardware cache for page table entries.
Virtual Address │ ▼ ┌─────────────┐ TLB HIT → Physical address immediately │ TLB │──────────────────────────────────────────► │ (cache) │ └──────┬──────┘ │ TLB MISS ▼ Page Table Walk (4 memory accesses) │ ▼ TLB filled with new translation Physical Address
TLB Performance: - Hit rate: typically 99%+ - Miss penalty: ~100 cycles (4 memory accesses) - TLB flush: happens on context switch (costly!) - PCID (Process Context IDs): avoids full TLB flush on modern CPUs# Monitor TLB misses (requires perf)perf stat -e dTLB-load-misses,iTLB-load-misses ./your_app
# High TLB misses suggest:# 1. Working set too large (poor cache locality)# 2. Many small pages (use HugePages — see Chapter 12)# 3. Too many context switches (causing TLB flushes)4.4 Page Faults
Section titled “4.4 Page Faults”A page fault occurs when a virtual address is accessed but the corresponding page is not present in physical RAM (the Present bit in the PTE is 0).
Types of Page Faults ───────────────────── 1. MINOR FAULT (soft fault): Page exists but wasn't mapped yet (demand paging, COW) → No disk I/O needed, just update page table → Very fast (~1 microsecond)
2. MAJOR FAULT (hard fault): Page exists but was swapped to disk → Requires disk I/O to bring page back → Very slow (milliseconds for SSD, seconds for HDD)
3. INVALID FAULT: Address is not mapped at all (bug in program) → Kernel sends SIGSEGV to the process → Process typically dies with "Segmentation fault"# Monitor page faults for a processps -o pid,maj_flt,min_flt,comm -p <PID>
# Detailed per-second statspidstat -r -p <PID> 1
# System-wide page fault ratevmstat 1# minflt/s and majflt/s columns
# Production alert: high major faults = swapping = memory pressure# Check if swapping is happeningvmstat 1 | awk '$7 > 0 || $8 > 0 {print "SWAPPING:", $0}'4.5 The Page Cache: Linux’s Performance Superpower
Section titled “4.5 The Page Cache: Linux’s Performance Superpower”The page cache is one of Linux’s most important features. It uses unused RAM to cache file data, dramatically accelerating I/O.
Without Page Cache: ────────────────── Application reads file → Every read goes to disk → NVMe: 100μs, HDD: 5ms
With Page Cache: ───────────────── Application reads file for first time: → Miss: reads from disk, stores in page cache
Application reads same file again: → HIT: returns from RAM immediately (<1μs!) → 100-5000x faster!
Linux Page Cache Size = All unused RAM ───────────────────────────────────── ┌────────────────────────────────────────────────┐ │ 16 GB RAM │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ OS/ │ │ App code │ │ PAGE CACHE │ │ │ │ Kernel │ │ and data │ │ (all the │ │ │ │ 256MB │ │ 2GB │ │ rest: 13GB) │ │ │ └──────────┘ └──────────┘ └──────────────┘ │ └────────────────────────────────────────────────┘
The page cache grows as needed and shrinks when applications need more RAM (it's fully reclaimed).Understanding Memory Output
Section titled “Understanding Memory Output”cat /proc/meminfo# MemTotal: 16384000 kB ← Total physical RAM# MemFree: 512000 kB ← Completely unused RAM# MemAvailable: 12000000 kB ← Available for new apps (incl. cache)# Buffers: 256000 kB ← Block device I/O cache (metadata)# Cached: 10000000 kB ← Page cache (file data)# SwapCached: 0 kB ← Swap in RAM (recently de-swapped)# SwapTotal: 2048000 kB# SwapFree: 2048000 kB# Dirty: 12800 kB ← Pages written but not yet flushed to disk
# The KEY metric: MemAvailable (not MemFree!)# MemFree = completely unused RAM# MemAvailable = MemFree + reclaimable cache# Always use MemAvailable to determine actual free memory
free -h# total used free shared buff/cache available# Mem: 15Gi 2.5Gi 512Mi 245Mi 12Gi 12Gi# Swap: 2.0Gi 0Bi 2.0GiControlling the Page Cache
Section titled “Controlling the Page Cache”# Drop the page cache (never do this in production without thought!)# 1 = drop page cache only# 2 = drop dentries and inodes# 3 = drop all threesync # First flush dirty pagesecho 3 > /proc/sys/vm/drop_caches # Then drop cache
# Check how much of a file is cached# Install fincore or use vmtouchvmtouch -v /var/log/nginx/access.log# Files: 1# Directories: 0# Resident Pages: 4096/65536 (6.25%) ← Only 6.25% in cache# Elapsed: 0.003 seconds
# Lock critical files into cache (prevent eviction)vmtouch -l /opt/myapp/critical_data.db
# Monitor dirty page writebackcat /proc/vmstat | grep -E "dirty|writeback"4.6 The Slab Allocator
Section titled “4.6 The Slab Allocator”The kernel needs to frequently allocate and free small objects (process descriptors, file objects, socket buffers). General-purpose malloc() would be too slow. The slab allocator pre-allocates pools of same-sized objects.
Slab Allocator Architecture ────────────────────────────
"I need a task_struct" (process descriptor, ~4KB) │ ▼ ┌─────────────────────────────────────────────────┐ │ task_struct SLAB CACHE │ │ │ │ Slab 1: [ts1][ts2][ts3][free][free][free] │ │ Slab 2: [ts4][free][free][free][free][free] │ │ Slab 3: [free][free][free][free][free][free] │ └─────────────────────────────────────────────────┘ │ ▼ Returns pre-allocated object from free slot (Extremely fast, no page table changes needed)
Benefits: ✓ No fragmentation (objects fit exactly) ✓ Pre-allocated (no allocation latency) ✓ Hardware cache-friendly (aligned) ✓ Poison/debug mode for detecting use-after-free bugs# View slab cache statisticscat /proc/slabinfo# orslabtop # Interactive slab view
# Example: finding a memory leak via slab growthwatch -n 5 'cat /proc/slabinfo | head -5 && grep tcp /proc/slabinfo'
# If "nf_conntrack" slab keeps growing → connection tracking table leak# Fix: tune net.netfilter.nf_conntrack_max or find the bug4.7 OOM Killer: When RAM Runs Out
Section titled “4.7 OOM Killer: When RAM Runs Out”When the system runs out of memory and swap, the kernel’s OOM (Out Of Memory) killer selects a process to kill.
OOM Killer Decision Process ────────────────────────────
Memory exhausted: ┌──────────────────────────────────────────────────┐ │ System has: 0 free pages + 0 swap │ │ A process requested more memory │ └──────────────────────────────────────────────────┘ │ ▼ For each process, calculate OOM score: ┌──────────────────────────────────────────────────┐ │ oom_score ≈ (memory_used / total_RAM) × 100 │ │ + adjustments for: │ │ - Root processes (lower score) │ │ - Processes using swap (higher score) │ │ - OOM score adjustment (/proc/PID/oom_score_adj)│ └──────────────────────────────────────────────────┘ │ ▼ Kill process with HIGHEST oom_score → Sends SIGKILL → Logs: "Out of memory: Kill process <PID> score <N>"# Check OOM scores of running processesfor pid in $(ls /proc | grep '^[0-9]'); do score=$(cat /proc/$pid/oom_score 2>/dev/null) comm=$(cat /proc/$pid/comm 2>/dev/null) [ -n "$score" ] && echo "$score $pid $comm"done | sort -n | tail -20
# Adjust OOM priority for critical processes# Range: -1000 (never kill) to 1000 (kill first)
# Protect critical daemon from OOM killerecho -1000 > /proc/$(pidof sshd)/oom_score_adj
# Make a process more likely to be killed (memory hog you can afford to lose)echo 500 > /proc/$(pidof memcached)/oom_score_adj
# Persist via systemd service:# In [Service] section:OOMScoreAdjust=-900 # High protectionOOMScoreAdjust=500 # More killable
# Detect OOM kills in logsjournalctl -k | grep -i "oom\|out of memory\|killed process"dmesg | grep -i "oom\|killed process"
# Real OOM log example:# Out of memory: Kill process 12345 (java) score 847 or sacrifice child# Killed process 12345 (java) total-vm:4096MB, anon-rss:3800MBOOM Killer in Production: Prevention
Section titled “OOM Killer in Production: Prevention”# Never use overcommit (risky but prevents surprise OOM)vm.overcommit_memory = 2 # 0=heuristic, 1=always, 2=nevervm.overcommit_ratio = 80 # 80% of RAM+swap can be committed
# OR: Keep overcommit but be aggressive about killingvm.panic_on_oom = 0 # Don't panic on OOM (kill instead)vm.oom_kill_allocating_task = 1 # Kill the allocating task (faster)
# Set up monitoring alert# Alert when memory available < 10%awk '/MemAvailable/ {avail=$2} /MemTotal/ {total=$2} END { pct = avail/total*100 if (pct < 10) print "WARNING: Only " pct "% memory available"}' /proc/meminfo4.8 Memory Analysis Tools
Section titled “4.8 Memory Analysis Tools”# ── Process Memory Analysis ──────────────────────────────# RSS: Resident Set Size (physical RAM used)# VSZ: Virtual Size (virtual memory claimed)ps aux --sort=-%mem | head -15
# Detailed memory breakdown for a processcat /proc/<PID>/status | grep -E "VmRSS|VmSize|VmSwap|VmPeak"
# smaps: detailed per-mapping memory usagecat /proc/<PID>/smaps | grep -E "^[0-9a-f]|Pss|Rss|Size"
# pmap: human-readable memory mappmap -x <PID>
# ── System Memory Analysis ───────────────────────────────# Overall memory statsfree -hvmstat -s | head -20
# Memory usage by typecat /proc/meminfo
# Slab memory usageslabtop
# ── Memory Leak Detection ────────────────────────────────# Monitor a process's RSS over timewhile true; do RSS=$(cat /proc/<PID>/status | grep VmRSS | awk '{print $2}') echo "$(date): RSS=${RSS}KB" sleep 60done
# Use Valgrind (development/staging only)valgrind --leak-check=full ./myapp
# Use eBPF for production-safe memory tracking# (see Chapter 24: perf & eBPF)4.9 Real-World Production Scenarios
Section titled “4.9 Real-World Production Scenarios”Scenario 1: Memory Leak in Production
Section titled “Scenario 1: Memory Leak in Production”A production Java service’s heap grew from 2GB to 8GB over 48 hours. Here’s the investigation:
# Step 1: Confirm memory growthwatch -n 30 'ps aux | grep java | awk "{print \$6}" | paste -s -d+ | bc'
# Step 2: Look for OOM warningsjournalctl -k --since "2 days ago" | grep -i "oom\|memory"
# Step 3: Check if it's heap or off-heap# For Java, get heap dumpjcmd <PID> VM.native_memoryjmap -heap <PID>
# Step 4: If heap → JVM memory leak# Enable GC logging: -Xlog:gc*:file=/var/log/gc.log:time# Or use JFR (Java Flight Recorder):jcmd <PID> JFR.start duration=60s filename=/tmp/flight.jfr
# Step 5: If it's off-heap (native memory leak)# Use jemalloc with profilingLD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 java ...# Export prof: MALLOC_CONF=prof:true,prof_prefix:/tmp/jeprof
# Root cause found: A thread pool was growing unboundedly# due to a missing thread pool size limit configuration.# Fix: Set ThreadPoolExecutor maximumPoolSizeScenario 2: High Memory Usage but No Leak
Section titled “Scenario 2: High Memory Usage but No Leak”# Server shows 95% RAM used, team panicsfree -h# total used free shared buff/cache available# Mem: 64Gi 58Gi 512Mi 1.2Gi 5.3Gi 5.8Gi
# But available = 5.8GB → not actually out of memory!# The "used" column includes page cache
# Calculate true application memory:APP_MEM=$(ps aux | grep -v "^USER\|grep\|ps" | \ awk '{sum += $6} END {print sum/1024/1024 " GB"}')echo "Application RAM: $APP_MEM"
# Most of the "used" RAM is page cache from PostgreSQL data files# PostgreSQL benefits from this! The kernel is caching its data.
# The correct metric to monitor:awk '/MemAvailable/ {avail=$2} /MemTotal/ {total=$2} END { print "Available: " avail/1024 " MB (" avail/total*100 "%)"}' /proc/meminfo4.10 Best Practices
Section titled “4.10 Best Practices”| Practice | Why | How |
|---|---|---|
| Monitor MemAvailable, not MemFree | Free is misleading | Alert on MemAvailable < 10% |
| Set OOM score for critical services | Protect sshd, monitoring | OOMScoreAdjust=-900 in systemd |
| Configure swap (even on large RAM) | Protects against OOM kill on rare spikes | 2GB swap, SSD |
Don’t use vm.overcommit_memory=1 in production | Silent OOM kills | Use default (0) or 2 |
| Monitor dirty page ratio | Avoid I/O storms during writeback | Alert on vm.dirty_ratio approach |
| Use HugePages for databases | Reduce TLB pressure | See Chapter 12 |
4.11 Interview Questions
Section titled “4.11 Interview Questions”Q1: What is the difference between MemFree and MemAvailable in /proc/meminfo?
Answer:
MemFreeis RAM that is completely unused by anything.MemAvailableis an estimate of RAM available for starting new applications — it includes MemFree plus page cache and reclaimable slab memory. In practice, the kernel will reclaim page cache if an application needs RAM, so monitoringMemFreewould give false alerts. You should always useMemAvailablefor memory pressure monitoring. A system with 100MB MemFree but 10GB MemAvailable is not in trouble.
Q2: Explain the Linux page cache. What are the implications for production database performance?
Answer: The page cache is the kernel’s cache for file data. When you read a file, the kernel reads the pages from disk and stores them in page cache (unused RAM). Subsequent reads of the same data come from RAM instead of disk, which is 100-1000x faster. For databases like PostgreSQL, this means the “hot” data pages (frequently accessed rows, indexes) will live in the page cache, effectively turning them into an in-memory database. This is why a PostgreSQL server with 64GB RAM and 100GB database but with the hot working set <64GB can perform as if everything were in memory. The implication: don’t set
MemoryMaxin the database’s systemd service to a value that starves the page cache.
Q3: What is the OOM killer and how would you protect critical services from it?
Answer: The OOM (Out Of Memory) killer is a kernel mechanism that kills processes when the system runs out of memory and swap. It selects processes to kill based on their
oom_score, which is roughly proportional to their RAM usage. To protect critical services: (1) SetOOMScoreAdjust=-1000in their systemd unit file to make them immune to OOM kills. (2) SetOOMScoreAdjust=+500for expendable services (caches, workers) to make them more likely to be killed first. (3) Monitor memory pressure proactively and add more RAM or reduce service footprint before OOM situations arise. (4) For JVM applications, set-Xmxexplicitly to prevent unbounded heap growth.
4.12 Summary
Section titled “4.12 Summary”| Concept | Key Point |
|---|---|
| Virtual Memory | Each process sees own address space, isolated |
| Page Tables | Translate virtual → physical addresses |
| TLB | Cache for page table translations |
| Page Fault | Minor (fast) or Major (needs disk I/O) |
| Page Cache | File data cached in unused RAM — highly beneficial |
| Slab Allocator | Efficient kernel object allocation |
| OOM Killer | Kills processes when RAM exhausted |
Next Chapter: Chapter 5: Cgroups, Namespaces & Container Primitives
Section titled “Next Chapter: Chapter 5: Cgroups, Namespaces & Container Primitives”Prerequisites
Section titled “Prerequisites”Chapter 1 (Architecture), Chapter 3 (Process Lifecycle) — understand process address spaces.
Advantages & Disadvantages
Section titled “Advantages & Disadvantages”Advantages: Virtual memory prevents cross-process corruption, page cache drastically speeds up disk I/O. Disadvantages: OOM (Out Of Memory) killer can unpredictably kill critical processes, fragmentation over time.
Common Mistakes
Section titled “Common Mistakes”- Reading
MemFreeas available memory — Linux uses free RAM for page cache.MemAvailableis the correct field. - Disabling swap entirely — even with 128GB RAM, swap prevents immediate OOM kills when memory spikes; cold pages move to swap, hot pages stay in RAM.
- Confusing RSS and VSZ: VSZ includes all mapped memory; RSS is actually resident in RAM. PSS (Proportional) is most accurate for shared libs.
- Assuming
malloc()allocates physical RAM — Linux is optimistic: malloc reserves virtual space; physical pages assigned on first write (demand paging). - Leaving THP (Transparent Huge Pages)
alwaysin production databases — compaction pauses cause latency spikes; usemadviseinstead.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Diagnosis | Fix |
|---|---|---|---|
| OOM killer fires unexpectedly | Memory overcommit ratio exceeded | `dmesg | grep -i ‘oom|killed process’` |
| Process memory grows unbounded | Memory leak — heap not returned to OS | `pmap -x PID | sort -k3 -rn |
| System I/O thrashing despite free RAM | Heavy swapping evicting active pages | vmstat 1 — check si/so; sar -B 1 | Lower vm.swappiness (default 60 → 10 for servers); add RAM |
| Huge page allocation fails | Memory too fragmented for contiguous 2MB pages | grep Huge /proc/meminfo; cat /proc/buddyinfo | Allocate huge pages at boot in GRUB; echo 1 > /proc/sys/vm/compact_memory |
Hands-on Lab
Section titled “Hands-on Lab”Objective: Understand memory usage at every level.
# 1. System-wide memory overviewfree -hcat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Cached|SwapTotal|SwapFree"
# 2. Top memory consumers (by PSS — most accurate)smem -r -k | head -10 # install smem if needed
# 3. Detailed process memory mapcat /proc/$$/smaps | grep -E "^(Vm|Rss|Pss|Private)" | head -30
# 4. Watch swap activityvmstat 1 5 # si=swap-in, so=swap-out (should be near 0)
# 5. Page cache impacttime cat /dev/urandom | head -c 1G > /tmp/bigfile # creates 1G filetime cat /tmp/bigfile > /dev/null # slow (first read)time cat /tmp/bigfile > /dev/null # fast (from cache)
# 6. OOM scoresfor pid in $(pgrep -x python3 | head -5); do echo "PID $pid: $(cat /proc/$pid/oom_score)"done
# 7. Huge page statusgrep -i huge /proc/meminfoExercises
Section titled “Exercises”- Write a C program that allocates 100MB, accesses every page, then calls
malloc_trim(0). Observe RSS before and after with/proc/PID/status. - Set
vm.overcommit_memory=2andvm.overcommit_ratio=80. Try to allocate more than 80% of RAM+swap. Observe what happens. - Use
dd if=/dev/zero of=/tmp/fill bs=1M count=512to fill the page cache. CheckCachedin/proc/meminfobefore and after. - Protect a critical process from OOM: set
echo -1000 > /proc/PID/oom_score_adj. Verify it survives a memory pressure test. - Configure 512 × 2MB huge pages at runtime. Write a C program that uses
mmap()withMAP_HUGETLBto allocate from them.
Revision Notes
Section titled “Revision Notes”- Virtual memory gives each process isolated address space — the MMU translates virtual → physical pages via page tables.
- Page cache uses all ‘free’ RAM to cache disk I/O —
MemAvailableaccounts for reclaimable cache, not justMemFree. - OOM killer selects the process with the highest
oom_scoreto kill — tune withoom_score_adj(-1000 = never kill, 1000 = kill first). - Swappiness: 60=default (swap when 40% used), 10=server (prefer RAM), 0=almost never swap.
- THP:
always=bad for DBs (compaction latency),madvise=safe (apps opt-in),never=use explicit huge pages. - Copy-on-Write, demand paging, and overcommit are the three pillars of Linux’s optimistic memory model.
Further Reading
Section titled “Further Reading”- Linux MM docs: https://www.kernel.org/doc/html/latest/admin-guide/mm/
- Understanding the Linux Virtual Memory Manager by Mel Gorman (free PDF)
- Brendan Gregg Memory Analysis: https://www.brendangregg.com/linuxperf.html#Memory
man 2 mmap,man 3 malloc— allocation interfaces
Related Chapters
Section titled “Related Chapters”- Chapter 3 — Process address space layout
- Chapter 5 — Memory limits via cgroups
- Chapter 12 — vm.* sysctl tuning
- Chapter 23 — Memory performance analysis
Last Updated: July 2026