Memory Management Thrashing — 120GB Working Set on 64GB
A Spark job's 120GB working set on 64GB server caused thrashing: 85% CPU on page faults, 200ms→4+ min queries.
20+ years shipping production systems from the metal up. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Memory management is the OS subsystem that allocates, tracks, and reclaims memory for running processes
- Physical memory is shared among processes; each process gets a private virtual address space
- Paging maps virtual pages to physical frames using page tables, enabling isolation and sparse use
- Virtual memory extends RAM to disk via swapping, with page replacement algorithms deciding what to evict
- Performance trap: TLB misses on context switch hurt throughput more than page faults in many workloads
- Biggest mistake: assuming virtual addresses match physical addresses — they never do in modern OSes
Imagine your desk is the computer's RAM — it's the space where you actually do your work. Your OS is the office manager who decides which papers (programs) get desk space, where they go, and who gets kicked off when the desk is full. When the desk overflows, the manager quietly moves older papers to a filing cabinet (your hard drive) and brings them back when needed — you barely notice. That swap between desk and cabinet is exactly what virtual memory does.
Every program you run — a browser, a game, a database — needs memory to breathe. Without a fair, structured way to hand out that memory, one misbehaving app could read your bank app's data, a crashed process could corrupt the entire system, and you'd never be able to run more than one program at a time. Memory management is the silent contract that makes modern computing safe and multi-tasking possible.
The problem it solves is deceptively deep. Physical RAM is finite and shared. Process A shouldn't be able to peek into Process B's address space. The OS needs to allocate memory fast, reclaim it when a process exits, and give each program the illusion that it owns all the memory in the world — even when RAM is nearly full. Without a memory manager, none of that is possible.
By the end of this article you'll understand exactly how the OS partitions memory, why paging replaced older schemes, how virtual memory lets your laptop run 40 browser tabs on 8 GB of RAM, and what questions about memory management reveal in a system-design or OS interview. Let's dig in.
What is Memory Management in OS?
Memory management is the OS subsystem responsible for allocating and deallocating memory to processes, tracking which parts of memory are free or in use, and providing isolation so that one process cannot access another's data. At its core, it solves three problems: sharing of finite physical RAM, protection between processes, and translation from virtual to physical addresses. Every modern OS—Linux, Windows, macOS—implements memory management as part of the kernel, using hardware support from the CPU's MMU (Memory Management Unit). Without it, a bug in a browser could corrupt a password manager in memory, and multiprogramming would be impossible.
- The building has limited rooms (physical frames).
- Each tenant has their own numbering system (virtual addresses).
- The landlord (MMU) translates the tenant's room number (virtual address) into a real room (physical address).
- Many tenants can share a common area (shared memory).
- If too many tenants arrive, the landlord moves some stuff to a storage locker (swap).
Memory Allocation Strategies: Contiguous vs Non-Contiguous
Early operating systems used contiguous allocation: a process gets a single block of physical memory. That led to fragmentation and overcommit problems. Modern OSes use non-contiguous allocation via paging. But within a process, memory allocation requests (malloc) are served by the heap manager, which uses a mix of contiguity and segmentation. The two main strategies are:
- Contiguous (Fixed Partitioning): Each process gets a fixed-size block; simple but wastes memory (external fragmentation). Not used in general-purpose OSes.
- Non-Contiguous (Paging): Physical memory is split into fixed-size frames; processes get virtual pages that can map to any frame. This eliminates external fragmentation and enables virtual memory.
Additionally, Segmentation allows variable-sized logical chunks (code, data, stack) but suffers from external fragmentation unless combined with paging. Most modern systems (Linux, Windows) use paged segmentation where segments are further broken into pages.
Paging: The Mechanism Behind Virtual Memory
Paging divides virtual memory into fixed-size blocks called pages (typically 4KB on x86_64) and physical memory into frames of the same size. Each process has a page table that maps virtual page numbers to physical frame numbers. The MMU uses this page table to translate every memory access from virtual to physical address. When a process accesses a page not currently in physical memory, a page fault occurs, and the OS loads the page from disk (swap) or from the file system (demand paging).
Page tables themselves are hierarchical (e.g., 4-level page tables on x86_64) to avoid needing a flat table with billions of entries. The Translation Lookaside Buffer (TLB) caches recent translations to speed up address translation. On a context switch, the TLB must be flushed, which is why repeated context switching hurts performance.
perf stat -e page-faults to count them.\nHuge pages (HugeTLB or transparent huge pages) can reduce TLB misses by 10-30% for large working sets.\nRule: For databases and VMs, explicit huge pages (HugeTLB) are more predictable than transparent huge pages (THP).Virtual Memory: The Illusion of Infinite Memory
Virtual memory extends the concept of paging: each process gets a full virtual address space (e.g., 2^48 on x86_64), but only the parts actively needed are backed by physical memory. The rest sits on disk (swap). When a process accesses a virtual address that is not in RAM, the OS loads the corresponding page from disk into a freed frame—this is demand paging. If no free frames exist, the OS evicts a page to disk using a replacement algorithm (LRU, Clock, etc.). Virtual memory enables: - Running programs larger than physical RAM. - Sharing libraries and memory-mapped files. - Copy-on-write (COW) forking.
The key components: page table, swap space (disk area), page replacement algorithm, and the page fault handler.
- The library catalog (page table) tells where each book is.
- A book on the shelf = page in RAM; in the basement = on swap.
- Frequent trips to the basement (thrashing) mean patrons are referencing books that keep getting removed.
- To avoid thrashing, ensure the total working set of active patrons fits on the shelves.
sar -B to monitor pgpgin/s and pgpgout/s.\nCopy-on-write (COW) after fork can cause memory doubling if not managed (e.g., after fork, child modifies many pages). Use vfork or posix_spawn to avoid COW.\nRule: Set vm.max_map_count appropriately; default (65530) is too low for large-memory processes like Elasticsearch.Page Replacement Algorithms: Which Pages Get Evicted?
When physical memory is full and a page fault occurs, the OS must evict a victim page to disk. The replacement algorithm determines which page to remove. The goal is to minimize future page faults by evicting pages unlikely to be used soon.
LRU (Least Recently Used): Evict the page not accessed for the longest time. Requires hardware support (reference bits) or software approximation (e.g., Clock algorithm).
Clock (Second Chance): Use a circular list with a reference bit. Sweep through, clearing bits; if bit is already clear, evict. Efficient approximation of LRU.
Working Set Model: Estimate the set of pages a process is actively using; only keep that set in RAM. Prevents thrashing by adjusting degree of multiprogramming.
Other algorithms: FIFO (simple but suffers from Belady's anomaly), Optimal (unimplementable, used as comparison).
Fragmentation: The Silent Performance Killer
Fragmentation is what happens when the OS's memory allocation decisions come back to bite you. Two flavors ruin your day. Internal fragmentation wastes memory inside an allocated block. Give a 10KB process a fixed 16KB partition? You just blew 6KB on nothing. External fragmentation is worse: free memory exists, but it's scattered into tiny, unusable holes. After enough allocate-free cycles, you get a Swiss cheese heap. No single free block can satisfy a large request. Your system starts thrashing, swapping pages like a gambler chasing losses. The fix? Non-contiguous schemes like paging. By breaking memory into fixed-size frames and processes into pages, the OS sidesteps external fragmentation entirely. Internal fragmentation becomes minimal — at most one partial page per process. If you're still seeing allocation failures on a box with free memory, check your fragmentation. It's the first thing I grep for in a production outage.
Modern Memory Allocation: jemalloc, tcmalloc, mimalloc
Traditional malloc implementations like glibc's ptmalloc2 can suffer from fragmentation and scalability issues under multi-threaded workloads. Modern allocators address these problems with per-thread caches, lock-free data structures, and optimized memory layouts.
jemalloc (used by Facebook, Redis, Firefox) reduces fragmentation by using separate arenas for different thread groups and maintaining size classes with buddy allocation. It excels in multi-threaded environments by minimizing contention.
tcmalloc (Google's allocator) uses thread-local caches and a central heap. It batches small allocations into pages and uses a page-level free list. This reduces lock contention and improves cache locality.
mimalloc (Microsoft) focuses on free list sharding and eager page purging. It uses a compact metadata structure and a novel 'free list of free lists' approach to reduce fragmentation.
Practical Example: Consider a web server handling 1000 concurrent requests, each allocating and freeing small objects. With ptmalloc, lock contention on the central heap can cause thrashing. Switching to jemalloc reduces contention by using per-thread arenas, improving throughput by 30%.
Production Insight: When migrating to a modern allocator, benchmark with realistic workloads. For example, Redis uses jemalloc by default; switching to mimalloc improved latency by 5% in some tests. Always test with your specific allocation patterns.
Huge Pages and Transparent Huge Pages in Linux
Huge pages reduce TLB misses by mapping large contiguous memory regions with fewer page table entries. Standard 4KB pages can cause TLB thrashing for workloads with large working sets (e.g., databases, VMs). Linux supports 2MB and 1GB huge pages.
Explicit Huge Pages: Reserved at boot or via /proc/sys/vm/nr_hugepages. Applications use mmap with MAP_HUGETLB or hugetlbfs. This guarantees huge pages but requires manual management.
Transparent Huge Pages (THP): Automatically promotes eligible 4KB pages to 2MB huge pages. Enabled by default on many distributions. However, THP can cause latency spikes due to compaction (memory defragmentation) and increased memory usage.
Practical Example: A Redis instance with 50GB dataset on a 64GB machine. Without huge pages, TLB misses cause 5% CPU overhead. Enabling THP reduces TLB misses by 80%, but compaction pauses increase latency by 2ms. Using explicit huge pages avoids compaction but requires memory reservation.
Production Insight: For latency-sensitive applications, disable THP (echo never > /sys/kernel/mm/transparent_hugepage/enabled) and use explicit huge pages. For throughput-oriented workloads, THP can be beneficial. Monitor /proc/meminfo for HugePages_Total and HugePages_Free.
Memory Overcommit and OOM Killer
Linux overcommits memory by default: it allows processes to allocate more virtual memory than physical RAM + swap. This relies on the fact that applications rarely use all allocated memory. However, when actual memory usage exceeds capacity, the Out-Of-Memory (OOM) Killer terminates processes to free memory.
Overcommit Modes: - 0 (heuristic overcommit): Based on overcommit ratio. - 1 (always overcommit): Never refuse malloc. - 2 (no overcommit): Fail if allocation exceeds commit limit.
OOM Killer Selection: Based on oom_score (badness) which considers memory usage, runtime, and root privileges. The process with the highest score is killed.
Practical Example: A Java application with -Xmx80G on a 64GB machine. With overcommit enabled, the JVM starts successfully. When the application actually uses 70GB and swap is full, the OOM Killer kills a random process (e.g., SSH daemon). To avoid this, set vm.overcommit_memory=2 and vm.overcommit_ratio=50 (50% of RAM+swap).
Production Insight: For critical services, disable overcommit (vm.overcommit_memory=2) and set vm.overcommit_ratio appropriately. Monitor /proc/meminfo for Committed_AS (total committed memory). Use cgroups to limit memory per process and avoid OOM killing unrelated services.
The Thrashed Production Server — When the OS Spends More Time Swapping Than Working
memory.soft_limit_in_bytes in cgroup to deprioritize batch jobs before they cause thrashing.- Thrashing happens when total working set exceeds physical RAM, not when RAM is 'full' — always monitor page fault rates (ps -eo min_flt,maj_flt)
- Use cgroups and ulimits to prevent one rogue process from starving the system
- Set vm.swappiness low (1-10) on latency-sensitive servers; never let the OS swap application pages
dmesg | grep -i oom | tail -5cat /proc/meminfo | grep -E '^MemTotal|^MemFree|^Cached'| File | Command / Code | Purpose |
|---|---|---|
| io | int main() { | What is Memory Management in OS? |
| io | struct page_table_entry { | Memory Allocation Strategies |
| io | int main() { | Paging |
| io | from collections import OrderedDict | Virtual Memory |
| io | struct frame { | Page Replacement Algorithms |
| fragmentation_check.c | void check_contiguous_fragmentation(size_t *free_blocks, int count, size_t neede... | Fragmentation |
| allocator_benchmark.c | void* worker(void* arg) { | Modern Memory Allocation |
| hugepage_setup.sh | echo 512 > /proc/sys/vm/nr_hugepages | Huge Pages and Transparent Huge Pages in Linux |
| oom_config.sh | echo 2 > /proc/sys/vm/overcommit_memory | Memory Overcommit and OOM Killer |
Key takeaways
Interview Questions on This Topic
Explain how virtual memory works. What happens when a process accesses a page not in RAM?
Frequently Asked Questions
20+ years shipping production systems from the metal up. Drawn from code that ran under real load.
That's Operating Systems. Mark it forged?
6 min read · try the examples if you haven't