Skip to content

Memory Management, Virtual Memory & Page Cache

Chapter 4: Memory Management, Virtual Memory & Page Cache

Section titled “Chapter 4: Memory Management, Virtual Memory & Page Cache”
  • 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

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.

Without memory management, processes would:

  1. Directly share physical RAM → any process could read another’s passwords
  2. Be limited to available physical RAM → no more than RAM can be used
  3. 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)

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
Terminal window
# 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)

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"
Terminal window
# Monitor page faults for a process
ps -o pid,maj_flt,min_flt,comm -p <PID>
# Detailed per-second stats
pidstat -r -p <PID> 1
# System-wide page fault rate
vmstat 1
# minflt/s and majflt/s columns
# Production alert: high major faults = swapping = memory pressure
# Check if swapping is happening
vmstat 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).
Terminal window
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.0Gi
Terminal window
# Drop the page cache (never do this in production without thought!)
# 1 = drop page cache only
# 2 = drop dentries and inodes
# 3 = drop all three
sync # First flush dirty pages
echo 3 > /proc/sys/vm/drop_caches # Then drop cache
# Check how much of a file is cached
# Install fincore or use vmtouch
vmtouch -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 writeback
cat /proc/vmstat | grep -E "dirty|writeback"

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
Terminal window
# View slab cache statistics
cat /proc/slabinfo
# or
slabtop # Interactive slab view
# Example: finding a memory leak via slab growth
watch -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 bug

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>"
Terminal window
# Check OOM scores of running processes
for 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 killer
echo -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 protection
OOMScoreAdjust=500 # More killable
# Detect OOM kills in logs
journalctl -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:3800MB
/etc/sysctl.d/99-oom.conf
# Never use overcommit (risky but prevents surprise OOM)
vm.overcommit_memory = 2 # 0=heuristic, 1=always, 2=never
vm.overcommit_ratio = 80 # 80% of RAM+swap can be committed
# OR: Keep overcommit but be aggressive about killing
vm.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/meminfo

Terminal window
# ── 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 process
cat /proc/<PID>/status | grep -E "VmRSS|VmSize|VmSwap|VmPeak"
# smaps: detailed per-mapping memory usage
cat /proc/<PID>/smaps | grep -E "^[0-9a-f]|Pss|Rss|Size"
# pmap: human-readable memory map
pmap -x <PID>
# ── System Memory Analysis ───────────────────────────────
# Overall memory stats
free -h
vmstat -s | head -20
# Memory usage by type
cat /proc/meminfo
# Slab memory usage
slabtop
# ── Memory Leak Detection ────────────────────────────────
# Monitor a process's RSS over time
while true; do
RSS=$(cat /proc/<PID>/status | grep VmRSS | awk '{print $2}')
echo "$(date): RSS=${RSS}KB"
sleep 60
done
# Use Valgrind (development/staging only)
valgrind --leak-check=full ./myapp
# Use eBPF for production-safe memory tracking
# (see Chapter 24: perf & eBPF)

A production Java service’s heap grew from 2GB to 8GB over 48 hours. Here’s the investigation:

Terminal window
# Step 1: Confirm memory growth
watch -n 30 'ps aux | grep java | awk "{print \$6}" | paste -s -d+ | bc'
# Step 2: Look for OOM warnings
journalctl -k --since "2 days ago" | grep -i "oom\|memory"
# Step 3: Check if it's heap or off-heap
# For Java, get heap dump
jcmd <PID> VM.native_memory
jmap -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 profiling
LD_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 maximumPoolSize
Terminal window
# Server shows 95% RAM used, team panics
free -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/meminfo

PracticeWhyHow
Monitor MemAvailable, not MemFreeFree is misleadingAlert on MemAvailable < 10%
Set OOM score for critical servicesProtect sshd, monitoringOOMScoreAdjust=-900 in systemd
Configure swap (even on large RAM)Protects against OOM kill on rare spikes2GB swap, SSD
Don’t use vm.overcommit_memory=1 in productionSilent OOM killsUse default (0) or 2
Monitor dirty page ratioAvoid I/O storms during writebackAlert on vm.dirty_ratio approach
Use HugePages for databasesReduce TLB pressureSee Chapter 12

Q1: What is the difference between MemFree and MemAvailable in /proc/meminfo?

Answer: MemFree is RAM that is completely unused by anything. MemAvailable is 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 monitoring MemFree would give false alerts. You should always use MemAvailable for 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 MemoryMax in 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) Set OOMScoreAdjust=-1000 in their systemd unit file to make them immune to OOM kills. (2) Set OOMScoreAdjust=+500 for 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 -Xmx explicitly to prevent unbounded heap growth.


ConceptKey Point
Virtual MemoryEach process sees own address space, isolated
Page TablesTranslate virtual → physical addresses
TLBCache for page table translations
Page FaultMinor (fast) or Major (needs disk I/O)
Page CacheFile data cached in unused RAM — highly beneficial
Slab AllocatorEfficient kernel object allocation
OOM KillerKills processes when RAM exhausted

Chapter 1 (Architecture), Chapter 3 (Process Lifecycle) — understand process address spaces.


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.


  • Reading MemFree as available memory — Linux uses free RAM for page cache. MemAvailable is 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) always in production databases — compaction pauses cause latency spikes; use madvise instead.

SymptomCauseDiagnosisFix
OOM killer fires unexpectedlyMemory overcommit ratio exceeded`dmesggrep -i ‘oom|killed process’`
Process memory grows unboundedMemory leak — heap not returned to OS`pmap -x PIDsort -k3 -rn
System I/O thrashing despite free RAMHeavy swapping evicting active pagesvmstat 1 — check si/so; sar -B 1Lower vm.swappiness (default 60 → 10 for servers); add RAM
Huge page allocation failsMemory too fragmented for contiguous 2MB pagesgrep Huge /proc/meminfo; cat /proc/buddyinfoAllocate huge pages at boot in GRUB; echo 1 > /proc/sys/vm/compact_memory

Objective: Understand memory usage at every level.

Terminal window
# 1. System-wide memory overview
free -h
cat /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 map
cat /proc/$$/smaps | grep -E "^(Vm|Rss|Pss|Private)" | head -30
# 4. Watch swap activity
vmstat 1 5 # si=swap-in, so=swap-out (should be near 0)
# 5. Page cache impact
time cat /dev/urandom | head -c 1G > /tmp/bigfile # creates 1G file
time cat /tmp/bigfile > /dev/null # slow (first read)
time cat /tmp/bigfile > /dev/null # fast (from cache)
# 6. OOM scores
for pid in $(pgrep -x python3 | head -5); do
echo "PID $pid: $(cat /proc/$pid/oom_score)"
done
# 7. Huge page status
grep -i huge /proc/meminfo

  1. Write a C program that allocates 100MB, accesses every page, then calls malloc_trim(0). Observe RSS before and after with /proc/PID/status.
  2. Set vm.overcommit_memory=2 and vm.overcommit_ratio=80. Try to allocate more than 80% of RAM+swap. Observe what happens.
  3. Use dd if=/dev/zero of=/tmp/fill bs=1M count=512 to fill the page cache. Check Cached in /proc/meminfo before and after.
  4. Protect a critical process from OOM: set echo -1000 > /proc/PID/oom_score_adj. Verify it survives a memory pressure test.
  5. Configure 512 × 2MB huge pages at runtime. Write a C program that uses mmap() with MAP_HUGETLB to allocate from them.

  • 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 — MemAvailable accounts for reclaimable cache, not just MemFree.
  • OOM killer selects the process with the highest oom_score to kill — tune with oom_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.



Last Updated: July 2026