What these two sysctls actually do

Mockup of /proc/meminfo CommitLimit and the three sysctls in a terminal

vm.overcommit_memory and vm.overcommit_ratio are the two knobs the Linux kernel exposes for virtual memory commitment accounting — the accounting layer that decides whether your process can be granted a virtual address range that may not have actual pages to back it.

The commit accounting sits on top of the page allocator. Each mmap(), brk(), mprotect() that turns a region writable, and mremap() adjusts a per-mm counter mm->committed_vm. The kernel tests that counter against a system-wide limit when the call would commit memory. Mode 0 does a heuristic; mode 1 always lets it through; mode 2 enforces a strictly-bounded limit defined by overcommit_ratio or overcommit_kbytes.

This is intentionally separate from cgroup memory limits. A process under memory.max=512M in cgroup v2 can still issue a 4 GB mmap(PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS) and have it succeed at commit accounting — cgroup will OOM-kill when the pages are actually faulted in. The two systems answer different questions:

Layer Decides based on Failure mode
Overcommit accounting A virtual reservation against a heuristic or fixed budget -ENOMEM from mmap() / malloc() (mostly safe)
cgroup memory controller Per-cgroup actual usage vs memory.high/memory.max OOM-kill (intrusive, possibly wrong process)
Page allocator slowpath Real physical pages via direct reclaim + PSI Wait, or OOM-kill at node level

If you’re running containerized workloads, overcommit_memory is the first decision, and cgroup is the second. They’re orthogonal and you can — and usually should — reason about them independently.

The three modes

Three-mode cards — heuristic / always / strict — with refusal semantics

Mode 0 — heuristic (default)

Heuristic overcommit handling. Obvious overcommits of address space are refused. Used for a typical system. It ensures a seriously wild allocation fails while allowing overcommit to reduce swap usage. This is the default.

The heuristics are in mm/util.c:__vm_enough_memory(). There’s a set of magic checks that mostly mean “if this is clearly dumb, refuse it”:

C
// mm/util.c, abbreviated
if (sysctl_overcommit_memory == OVERCOMMIT_NEVER)
    vm_committed_as_b = ...;
else if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
    return 0;                 // always allow
else {
    // heuristic — magic constants
    free = global_zone_page_state(NR_FREE_PAGES);
    free += global_zone_page_state(NR_FILE_PAGES);
    /* ... swap space, ... */
    if (free > pages)
        goto ok;
    if (cap_sys_admin)
        goto ok;             // root has a 3% slack here, git-blame has it
    /* lots of heuristics below — see source for the full list */
}

What matters is what is refused vs allowed:

  • Refused: allocations larger than ~3–8% of physical RAM in a single mmap by a non-root caller. Specifically, if vm.overcommit_memory=0 and the request would push vm_committed_as + bytes > some_huge_number_calculated_from_RAM, non-root callers get -ENOMEM. Root can do roughly 3% more than that. This is the heuristic that keeps malloc(64 * 1024 * 1024 * 1024) from succeeding.
  • Allowed: anything that “fits” within the rough bounds of (swap + a fraction of RAM). In particular, scientific workloads that allocate huge sparse regions get through.
  • MAP_NORESERVE: explicitly bypasses the check. Useful when you’re requesting huge regions where you really don’t want reservation (e.g., MPI shared heap).

The reason mode 0 is the default: it’s a middle ground. It catches the wild overcommits that would invite thrash and lets the rest through. Per-call cost: a few atomic ops and a math check. Cheap.

Mode 1 — always overcommit

Bash
echo 1 > /proc/sys/vm/overcommit_memory

C
// mode 1 branch
return 0;          // always allow

The kernel doesn’t even consult committed_vm or any limit. Every mmap() succeeds. Pages do not exist until a fault occurs; the OOM-killer is invoked if there’s not enough physical RAM to back the fault.

The classical use case the kernel docs cite:

Classic example is code using sparse arrays and just relying on the virtual memory consisting almost entirely of zero pages.

In a JVM with -Xmx=512G, the entire heap is committed up front but typically faulted only on demand. With mode 0 a 512G Java process on a 128G host would be refused (or refused in production-rerun cycle); with mode 1 it’s fine, and the OOM-killer picks the moment. The same pattern shows up in Redis (fork+copy-on-write), in-process caches that allocate huge buckets up front, and any scientific code that uses sparse addresses.

Mode 2 — strict accounting

Bash
echo 2 > /proc/sys/vm/overcommit_memory

Now there’s a hard limit. The kernel computes:

Bash
CommitLimit = (total_RAM − total_huge_TLB) × overcommit_ratio / 100 + total_swap

Every mmap() / brk()-with-extension increments vm_committed_as. If vm_committed_as + new_request > CommitLimit, the call returns -ENOMEM (in userspace: malloc() returns NULL; mmap() returns MAP_FAILED).

The famous gotcha is in the kernel docs:

In mode 2 the MAP_NORESERVE flag is ignored.

So even asking explicitly for no reservation doesn’t help. Mode 2 is strict.

Two percent knobs in this mode:

  • vm.overcommit_ratio — percentage (default 50). At overcommit_ratio=50, CommitLimit = (RAM − huge_TLB) × 0.5 + swap.
  • vm.overcommit_kbytes — absolute bytes. If non-zero, overrides ratio. Useful when you want to fix the absolute number regardless of RAM size (e.g. lockstep reservation for a container host).

A non-obvious point from the serverfault discussion: setting overcommit_ratio > 100 does overcommit. ratio=200 means CommitLimit = 2× RAM + swap. The doc’s “Don’t overcommit” is misleading — the kernel respects the limit, not whether the limit is below or above physical RAM.

Why overcommit is necessary

Without overcommit, every mmap() would have to reserve physical pages up front. That sounds reasonable until you realize what a typical application does:

C
// typical jvm launcher
void *heap = mmap(NULL, 512UL << 30 /* 512 GiB */,
                  PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// → fault on demand, backing pages allocated lazily

If the kernel had to back this with 512 GB of physical RAM, you’d never run a 512 GB configured JVM on anything smaller. The trade is: let virtual memory exceed physical memory, but provide a mechanism (overcommit refusal + OOM-kill) to handle the case where virtual memory exceeds physical reality. The kernel picks: refuse up front when it can, or kill a process later when it has to.

Reading the knobs and watching the budget

Bash
# The two sysctls:
sysctl vm.overcommit_memory vm.overcommit_ratio vm.overcommit_kbytes
# vm.overcommit_memory = 0
# vm.overcommit_ratio  = 50
# vm.overcommit_kbytes = 0

# What the kernel actually sees right now:
grep -i commit /proc/meminfo
# CommitLimit:    8466180 kB
# Committed_AS:   4235844 kB

CommitLimit is what was computed from overcommit_ratio and current RAM/swap (/proc/meminfo shows the live number, even if you don’t know the sysctl). Committed_AS is the running tally across all processes’ mm->committed_vm. If Committed_AS > CommitLimit you’re in trouble — well, in mode 0/1 it doesn’t matter because the check passed already; in mode 2 you’d have already failed that mmap call.

If you want to see who is consuming commit:

Bash
# Per-process breakdown of mm->committed_vm
for pid in /proc/[0-9]*; do
    if [[ -r "$pid/smaps" ]]; then
        committed=$(awk '/^Pss:|Rss:/{sum+=$2}END{print sum*1024}' "$pid/smaps")
        name=$(cat "$pid/comm" 2>/dev/null)
        printf "%-6s %-20s %s bytes\n" "$(basename $pid)" "$name" "$committed"
    fi
done | sort -k3 -n -r | head -20

Note that this is approximate — Pss is proportional set size, not committed. The actual committed_vm isn’t exported per-task in modern kernels; you have to sum from cgroup or use smem for a runtime picture.

The fork()-on-write Redis pattern

Redis’s BGSAVE/BGREWRITEAOF operations fork() the process. Linux uses copy-on-write: the child shares pages with the parent until either writes to one. If mode 0 is on and Redis’s parent is −Xmx=64G, the kernel sees the child’s full commit charge on top of the parent’s, which can refuse on systems short of memory.

This is the classic Redis tuning recommendation:

Bash
echo 1 > /proc/sys/vm/overcommit_memory

Mode 1 lets the fork succeed; on memory pressure, the OOM-killer picks. Combined with vm.overcommit_ratio=100 (or as Redis docs historically suggested), the system is permissive. This is widely cited in the Redis and database tuning documentation. Mode 1 is not the right answer for a multi-tenant host with mixed workloads — only for fork-heavy single-purpose daemons.

The Java/JVM case

Modern JVMs with -Xmx routinely commit the entire heap at startup. On a 128 GB host, a JVM with -Xmx=64G is fine; with -Xmx=192G, mode 0 will refuse on a 128 GB host, and even mode 1 may not save you if the kernel eventually OOM-kills the JVM at startup because physical pages don’t exist yet.

The practice on JVM hosts is to either:

  • Run mode 1 + ensure the sum of Xmx across all JVMs doesn’t exceed physical RAM.
  • Run mode 2 with overcommit_ratio=100 (or higher) so commit accounting is permissive enough.
  • Run mode 0 and tune the JVM to commit less (-XX:+UseCompressedOops, -XX:MaxRAM=N) so the committed virtual memory fits.

Modern distributions default to mode 0 even on JVM hosts because MAP_NORESERVE is respected. For predictable startup, the JVM packaging community has been pushing mode-2-with-high-ratio for years.

Mode 2 in container hosts

Kubernetes (and any other multi-tenant orchestrator) typically sets sysctl vm.overcommit_memory=1 on the host, plus per-pod cgroup memory.max. This combination:

  • Lets every pod’s mmap succeed even if their declared RSS exceeds physical RAM
  • Enforces per-pod RSS via cgroup
  • Lets the kernel OOM-kill at the cgroup level first, only escalating to the host if cgroup fails

Mode 2 with a sensible ratio is the older container-host recipe, and it still works. The practical difference is that mode 2 lets the operator budget the commit precisely, while mode 1 lets the kernel pick the victim at fault time.

Interaction with cgroup v2 memory controller

Layered protection: overcommit → page allocator → cgroup; production combinations per workload

If you’re reading this with the memory cgroup v2 article in mind, the question is naturally: where do these two systems overlap?

  • They don’t overlap in accounting. Overcommit is a virtual-reservation check on commit; cgroup is a physical-usage check.
  • Mode 2 with overcommit_ratio=50 on a host running cgroup v2 containers is conservative: every container’s mmap must fit within half of physical RAM + swap. This may limit what your containers can do, but it’s a way to refuse virtual memory growth up front.
  • Mode 1 + cgroup means commit accounting is permissive; real memory usage is constrained by cgroup at fault time.
  • The same __vm_enough_memory() call is the only thing mode 0 cares about — cgroup cares about physical usage via memcg_charge().

The combined pattern that works well in production:

Bash
host:    vm.overcommit_memory=1 (or 2 with high ratio)
pod env: memory.high = expected RSS * 1.5
         memory.max = bound that says "if you grow past this, you get killed"

That gives you layered protection: refusal up front if mode 2 + low ratio, and cgroup at fault time if permissive.

How mode 0’s heuristic actually works, in code

__vm_enough_memory() in mm/util.c is small but has lots of magic numbers. Simplified:

C
int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin)
{
    long allowed;
    unsigned long commit_charge;   // what we're tentatively charging
    unsigned long free, total;
    int overcommit;

    commit_charge = pages;

    /*
     * Overcommit limits are in pages; convert from bytes.
     */
    if (mm)
        commit_charge += mm->committed_vm;

    overcommit = sysctl_overcommit_memory;

    if (overcommit == OVERCOMMIT_ALWAYS)
        return 0;     // mode 1: always succeed

    if (overcommit == OVERCOMMIT_NEVER)
        goto out;     // mode 2: fall through to limit check

    /* Mode 0 (heuristic) below */

    free = global_zone_page_state(NR_FREE_PAGES);
    free += global_zone_page_state(NR_FILE_PAGES);
    /* swap and SReclaimable counted in the source */

    total = global_zone_page_state(NR_ACTIVE_ANON) +
            global_zone_page_state(NR_INACTIVE_ANON) +
            /* ...active+inactive file, etc. */

    /*
     * If the request fits in free + reusable pages, allow.
     * Otherwise, if cap_sys_admin, allow up to 3% overcommit slack.
     * The exact constants are git-blame historic.
     */
    if (free > pages)
        return 0;

    /* The "wildly excessive" check: refuse 81×commit_total? */
    if (cap_sys_admin)
        allowed = total / 32 + 1;
    else
        allowed = total / 16 + 1;

    if (commit_charge > allowed) {
        /* rarely fatal — this is where malloc returns NULL for big mmap()s */
        return -ENOMEM;
    }

    return 0;
}

Reading the source: the heuristic is essentially “the request would consume a noticeable chunk of physical memory” check. A 4 GB mmap on a 64 GB host passes; a 64 GB mmap on a 64 GB host is refused for non-root; a 64 GB mmap on a 128 GB host with enough free pages passes for non-root. The constants vary slightly between kernel versions but the structure is stable.

The gotchas

Mode change is instantaneous

Bash
echo 2 > /proc/sys/vm/overcommit_memory
# takes effect on next mmap, no restart needed

But there’s a subtle issue: a single process whose committed_vm > CommitLimit after the change is set does not get killed — it has its reservation. The change only refuses new commits. To drop existing commitments requires either process exit or explicit munmap(). The new limit applies going forward.

Stack growth is implicit mremap

From the docs:

The C language stack growth does an implicit mremap. If you want absolute guarantees and run close to the edge you MUST mmap your stack for the largest size you think you will need.

When a thread’s stack auto-grows under access, the kernel calls expand_stack()expand_downwards()mmap(), and that mmap participates in commit accounting. A thread blowing its stack in mode 2 might get -ENOMEM from the kernel for the auto-grow and SIGSEGV the thread. This is the most common “we set overcommit=2 and now programs crash” report.

Mitigation: ensure stack ulimits (ulimit -s) are explicit, and pre-touch sensitive stack variables in startup code. Or stay at mode 1.

/proc/meminfo CommitLimit is dynamic

CommitLimit is recomputed every time the formula inputs change (RAM hot-plug, swap change, sysctl update) or per-call in mode 0. You cannot rely on a snapshot — by the time you read it, it’s already slightly stale. Treat it as a noisy dashboard, not a precise billing number.

overcommit_kbytes overrides ratio

Setting both overcommit_kbytes=0 and overcommit_ratio=N ignores the ratio. To go back to ratio-based, set overcommit_kbytes=0.

MAP_NORESERVE is silently ignored in mode 2

The kernel’s lack of path-specific handling means asking for “no reservation” gets you nothing; the reservation is always made. This surprises syscall-level tools that expect mode 2 to back off when they explicitly waived reservation. Documented in Documentation/mm/overcommit-accounting.rst.

COW after fork — hidden growth

If Redis is in mode 1 and BGREWRITEAOF, then starts dirtying pages, the actual physical usage grows after the fork. If you’ve set the kernel to refuse based on commit, that refault will OOM-kill the child process despite the apparent headroom in commit. Mode 1 + physical-only monitoring (cgroup memory events) handles this case correctly.

Reading the limits live

Useful one-liners for an SRE:

Bash
# What we have / what we commit / what we cap:
awk '/^Commit/ {print}' /proc/meminfo
sysctl vm.overcommit_memory vm.overcommit_ratio vm.overcommit_kbytes

# Per-process top contributors (commit-styled):
smem -t -k -s commit
# Or with a manual pass:
for p in /proc/[0-9]*; do
    name=$(cat $p/comm 2>/dev/null)
    c=$(grep VmRSS $p/status 2>/dev/null | awk '{print $2}')
    [[ -n "$c" ]] && echo "${c} ${name}"
done | sort -n | tail -20 | awk '{printf "%.1f MB\t%s\n", $1/1024, $2}'

# A quick test: how much headroom am I actually using?
python3 -c "
ratio = 0.5; swap_kb=0
meminfo = dict(line.split(':',1) for line in open('/proc/meminfo') if ':' in line)
ram = int(meminfo['MemTotal'].split()[0])
total = int(meminfo['CommitLimit'].split()[0])
comm = int(meminfo['Committed_AS'].split()[0])
print(f'CommitLimit:    {total//1024} MiB')
print(f'Committed_AS:   {comm//1024} MiB ({100*comm/total:.1f}% of limit)')
print(f'Headroom:       {(total-comm)//1024} MiB')
"

Choosing the right setup per workload

Decision tree:

  1. Single-tenant host + fork-heavy daemon (Redis, classic fork-based PostgreSQL clone): vm.overcommit_memory=1. Don’t bother with ratio.
  2. Multi-tenant container host: vm.overcommit_memory=1 + cgroup v2 limits. Ratio is mostly noise.
  3. Single-tenant with huge JVM or sparse-array scientific code: vm.overcommit_memory=1. The OOM-killer is your friend at fault time.
  4. Hard real-time / embedded where you can’t tolerate random OOM kills: vm.overcommit_memory=2 + overcommit_ratio=100 (or kbytes tuned to your RAM+swap). Yes, programs will occasionally refuse big mmap; that’s the cost of guarantee.
  5. Default Linux desktop: vm.overcommit_memory=0. Default heuristic. Don’t touch it.

The conceptual switch: mode 0/1 give the kernel permission to kill processes as a last resort; mode 2 never kills (and reports failure up front instead). Pick mode 2 only when you’re willing to refuse allocations rather than kill.

Interaction with psi=0 and other VM tunings

If you’ve read about psi=0 (the cmdline that disables PSI accounting for performance) — that’s a separate system. PSI tracks stalls; overcommit tracks reservations. Setting psi=0 doesn’t change vm.overcommit_memory and vice versa. The only knotted case: on a system with psi=0, the OOM-kill decision won’t be visible via memory.pressure — you have to read /proc/meminfo CommitLimit / Committed_AS plus the OOM-killer log (dmesg or systemd journal).

What the kernel docs do not mention

A few things that show up in production but aren’t in the docs:

  • vm.swappiness and overcommit are orthogonal. swappiness controls how aggressively the kernel swaps out file-cache pages; overcommit is about virtual-reservation accounting. Tunable for the same reason (memory management), but the two don’t compose.
  • vm.zone_reclaim_mode doesn’t affect overcommit. It controls NUMA-local reclaim aggressiveness.
  • vm.watermark_scale_factor doesn’t either. That’s about kswapd aggressiveness.
  • The overcommit_memory knob is one of the few vm.* sysctls that’s safe to flip at runtime without affecting currently-running processes. Only future mmap calls care.

The actual gotcha list, in one place

  • Mode 2 ignores MAP_NORESERVE. Read the docs before you ask for “no reservation.”
  • Mode 2 with default overcommit_ratio=50 will refuse Java -Xmx larger than 2 × RAM + swap. Set ratio higher or use mode 1.
  • Mode 1 lets any mmap succeed. OOM-kill happens at fault. Don’t blame the OOM-killer.
  • vm.overcommit_kbytes overrides overcommit_ratio. Set kbytes back to 0 to revert.
  • Redis needs mode 1 for fork+COW to not OOM-kill on the BGREWRITEAOF path.
  • Container hosts almost always want mode 1 + cgroup limits, not mode 2.
  • Stack growth auto-mmap can fail in mode 2 and cause SIGSEGV. Bound ulimit -s explicitly in deep modes.
  • Commit accounting is a heap of heuristics and magic constants. Don’t expect mode 0 to behave like mode 1 with extra check; the mode 0 constants are not predictable across kernels.
  • The “obvious overcommits” the docs refer to in mode 0 are > ~6% of RAM in a single mmap by non-root. Set vm.overcommit_memory=1 if your workload legitimately needs > 6% per mmap and you trust the OOM-killer.

The full picture: virtual memory accounting is a contract between the kernel and userspace. Mode 0, 1, and 2 are three contracts with different “refuse” semantics. Pick the one whose refusal semantics match your tolerance for OOM kills vs allocation failures.

Last modified: 2026年7月21日

Author

Comments

Write a Reply or Comment

Your email address will not be published.