Memory pressure on macOS: compressor, jetsam, and the working set
How XNU responds when memory gets tight — the four-stage pressure pipeline from free pages through compression to swap to process termination, and what each stage costs.
A Mac with abundant RAM never thinks about memory pressure. Allocations always find free pages; the VM system runs in a relaxed steady state. Then something — a heavy build, a 50-tab browser, a video edit — pushes the working set past available RAM, and XNU's pressure pipeline springs into action.
This article walks the four stages: free pages → compressor → swap → jetsam.
The starting state: free, active, inactive
XNU tracks every physical page in one of a few queues:
- Free — unmapped, available for immediate allocation.
- Active — mapped and recently referenced.
- Inactive — mapped but not recently referenced; first candidates for reclaim.
- Speculative — read-ahead pages that nothing has actually used yet.
- Wired — kernel-pinned, can't be paged out.
The page-replacement algorithm is a variant of "two-handed clock" — a daemon thread (vm_pageout) walks the active queue, demoting pages that don't carry a recent-access bit to the inactive queue. When the free queue runs low, the pageout daemon reclaims from inactive first.
Stage 1: compressor (the soft swap)
When free pages drop below a low-watermark threshold, the pageout daemon picks inactive pages and hands them to the VM compressor rather than swapping them to disk:
apple-oss-distributions/xnuosfmk/vm/vm_compressor.cThe compressor — WKdm + LZ4, in-RAM compressed page pool.View on GitHub(line —)The compressor:
- Takes the chosen page's contents.
- Compresses with WKdm (fast) — falls back to LZ4 for hard-to-compress pages.
- Stores the compressed blob in a dedicated pool.
- Frees the original physical page.
Typical real-world compression ratios are 2-4× on application memory. That means reclaiming 10 GB of inactive pages yields 5-8 GB of new free space and uses 2-5 GB of compressor pool.
When the process accesses the compressed page, a fault decompresses it back into a fresh physical page. The cost: a few microseconds of CPU per page versus tens of microseconds for an SSD read, no NAND write at all.
Activity Monitor → Memory → Compressed is the live compressor-pool size. While it's climbing and Swap Used is still zero, the system is handling pressure entirely in RAM.
Stage 2: swap files (the hard swap)
When the compressor pool itself fills up — typical at very high pressure — XNU spills compressed pages to actual swap files on disk:
apple-oss-distributions/xnuosfmk/vm/vm_compressor_swap_default.cSwap file management — when the compressor pool overflows.View on GitHub(line —)Swap files live under /private/var/vm/swapfile*. They're allocated dynamically (in 64 MB increments by default) and removed when not needed. Pages here are paged in via standard disk I/O — much slower than the compressor (milliseconds vs microseconds).
Modern Macs aggressively avoid swap because:
- SSD writes are finite. Swapping causes write amplification.
- Compressing in RAM is faster on Apple Silicon than reading from SSD.
- Mobile-derived design instincts: phones never swap.
If you see Swap Used > 0 climbing steadily, the system is past the compressor's capacity and is paying real I/O cost for memory.
Stage 3: file-backed eviction
Pages backed by a file (text segments of binaries, mmaped data files) don't need to go to swap — they can be discarded outright, since the original is still on disk. When the kernel evicts these:
- Clean pages (never modified): just unmap. Re-read from the file on next access.
- Dirty pages: write back to the file, then unmap. (Only happens for
MAP_SHAREDmappings;MAP_PRIVATEmodifications go to anonymous pages via copy-on-write.)
This is why a process whose resident set drops doesn't necessarily mean swap — text pages of dylibs can just be dropped, with the original Mach-O on disk as the always-available source.
Stage 4: jetsam
Even compression + swap can run out. When the kernel decides further reclamation is impossible or too expensive, it picks a process to terminate. This is jetsam:
apple-oss-distributions/xnubsd/kern/kern_memorystatus.cmemorystatus — the jetsam policy, memory bands, kill order.View on GitHub(line —)Every process has a jetsam priority band set at launch by launchd:
- Foreground app — top priority, killed last.
- Background app you can see — next.
- Hidden background app — middle.
- Background service — lower.
- Idle daemon — lowest, first to go.
When the system's free-memory falls below a band's threshold, the kernel kills the lowest-priority process in that band first. The killed process gets SIGKILL (no chance to clean up), and the killer reason shows up in Console as a jetsam log entry.
On a Mac with abundant RAM you'll never trip jetsam. On an 8 GB Mac running heavy creative apps you will, and you'll see your background browser tabs silently die — they're hidden, low-priority, expendable.
The working set
The working set of a process is the set of pages it accesses within a recent window. The pageout daemon tries to keep every process's working set resident — that's the implicit contract. Working sets are tracked per-process and per-coalition.
apple-oss-distributions/xnuosfmk/vm/vm_pageout.cvm_pageout — the page-replacement daemon, working-set tracker, jetsam trigger.View on GitHub(line —)When the total working set across all processes exceeds RAM, you have memory thrashing — the kernel can't keep everyone's working set resident, evicts pages that get faulted back in immediately, evicts those again, etc. CPU usage looks low (nothing is making progress), but I/O is saturated. On a Mac, this triggers jetsam well before traditional thrashing fully sets in.
Observing pressure: tools
vm_stat 1— per-second snapshot of every page queue, fault counts, compression activity. The best way to see pressure in real time.memory_pressure— generates synthetic pressure for testing app behavior.footprint— accurate per-process memory accounting (better thantop's RSS, which double-counts shared regions).- Activity Monitor → Memory → Memory Pressure — a smoothed scalar: green/yellow/red.
The kernel exposes memory-pressure notifications via dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, …) — well-behaved apps subscribe and free caches when fired.
Common surprises
- The "Memory Pressure" gauge is not just "RAM used." A system can be 90% used and still green if everything's neatly paged in.
- Compressed pages count as "used". The Memory column in Activity Monitor includes them.
- Jetsam is silent. A killed background process doesn't notify the user. The first sign is often "the app I switched away from re-launched."
- Wired memory can't be compressed or swapped. A kernel that wires too much memory creates pressure no userspace tuning can fix.
What to read next
apple-oss-distributions/xnubsd/sys/kern_memorystatus.hThe memorystatus interface — what userspace can query and what it can subscribe to.View on GitHub(line —) apple-oss-distributions/xnuosfmk/vm/vm_purgeable.cPurgeable memory — a category between anonymous and file-backed; the system can drop these at will.View on GitHub(line —)
And the virtual memory overview sets the stage; this article is the "what happens under pressure" extension.