Skip to content

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.

Published 5 min read
XNU memory pressure pipelineFour-stage memory pressure pipeline. As free memory drops, XNU progressively engages the compressor, then swap files, then jetsam to terminate background processes.FREE MEMORY DROPS → KERNEL RESPONSE ESCALATESplentycrisisStage 0 · idlefree queue healthyactive / inactivebalancedgauge: greenStage 1 · compressorcompress inactivepages with WKdm2-4× ratio · in RAMgauge: yellowStage 2 · swap filescompressor fullspill compressedpages to SSDgauge: yellow→redStage 3 · jetsamkill lowest-priorityprocess in currentmemory bandgauge: redlow watermarkcompressor fullkill thresholdno cost~µs CPU~ms disk I/OSIGKILL — process diesJetsam kill order — lowest priority firstIdle daemon < Background service < Hidden background app < Background app you can see < Foreground appHow to watch this in real timevm_stat 1 · memory_pressure · footprint · Activity Monitor → Memory

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.
apple-oss-distributions/xnuosfmk/vm/vm_resident.cThe page queues — alloc, deactivate, page steal.View on GitHub(line )

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:

  1. Takes the chosen page's contents.
  2. Compresses with WKdm (fast) — falls back to LZ4 for hard-to-compress pages.
  3. Stores the compressed blob in a dedicated pool.
  4. 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:

  1. Clean pages (never modified): just unmap. Re-read from the file on next access.
  2. Dirty pages: write back to the file, then unmap. (Only happens for MAP_SHARED mappings; MAP_PRIVATE modifications 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 than top'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.

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.

Related

Every macOS process gets a private address space it can't possibly afford. Here's how XNU gives it one anyway — pmap, vm_map, the compressor, and jetsam.
The kernel's own malloc — a hierarchy of zone allocators, the kalloc heap, and slab caches for specific types. Different from user-side VM, and just as important.
Walk a single mmap call from libc, through BSD into Mach VM, the lazy first-touch fault, and the pmap entry that finally makes the file accessible as memory.