Linux 进程内存管理深度报告

> 主题: Linux 进程内存管理机制全解析

> 适用范围: Linux x86_64 系统,内核 4.x-6.x

> 研究时间: 2026-03-29

一句话版本

Linux 进程看到的是虚拟地址空间,物理内存按需分配(lazy allocation),通过 page fault 触发真正的内存映射。理解 VSZ/RSS/PSS/USS 的区别、OOM Killer 的打分逻辑、swap 与 overcommit 策略,是排查内存问题的核心知识。

目录

1. 虚拟地址空间布局

2. 内存指标详解

3. 页面机制

4. Linux 内存回收与 OOM Killer

5. Swap 机制

6. 实用诊断命令

7. Node.js 特有的内存知识

1. 虚拟地址空间布局

每个 Linux 进程运行在自己的 虚拟地址空间 中。在 64 位系统上,用户空间通常占据低 128TB(0x0000000000000000 ~ 0x00007FFFFFFFFFFF),内核空间占高地址。

经典布局(高地址 → 低地址)


┌─────────────────────────────────┐ 0x7FFF_FFFF_FFFF (128TB)
│           内核空间               │ ← 用户态不可访问
├─────────────────────────────────┤
│           栈 (Stack)             │ ← 向下增长 ↓
│           ↓↓↓                   │
│                                 │
│      (随机间隔 - ASLR)          │
│                                 │
│     内存映射区 (mmap)            │ ← 共享库、mmap 文件
│     libpthread.so               │
│     libc.so                     │
│     ld-linux.so                 │
│                                 │
│      (随机间隔 - ASLR)          │
│                                 │
│           ↑↑↑                   │
│           堆 (Heap)             │ ← 向上增长 ↑ (brk/sbrk)
├─────────────────────────────────┤
│           BSS 段                │ ← 未初始化的全局/静态变量
├─────────────────────────────────┤
│           数据段 (Data)          │ ← 已初始化的全局/静态变量
├─────────────────────────────────┤
│           代码段 (Text)          │ ← 可执行指令,只读
└─────────────────────────────────┘ 0x0000_0040_0000 (通常起始)

各段详解

权限内容增长方向
**Text (代码段)**r-x (只读+可执行)编译后的机器指令固定大小
**Data (数据段)**rw- (可读写)已初始化的全局变量 `int x = 42;`固定大小
**BSS**rw-未初始化的全局变量 `int y;`,启动时清零固定大小
**Heap (堆)**rw-`malloc()`/`new` 动态分配向上增长 ↑
**mmap 区**各异共享库、`mmap()` 映射的文件/匿名内存向下增长 ↓
**Stack (栈)**rw-局部变量、函数调用帧、返回地址向下增长 ↓

ASLR (地址空间布局随机化)

Linux 默认启用 ASLR(/proc/sys/kernel/randomize_va_space = 2),每次程序运行时随机化栈、mmap、堆的起始地址,防止缓冲区溢出攻击利用固定地址。


# 查看 ASLR 设置
cat /proc/sys/kernel/randomize_va_space
# 0 = 关闭, 1 = 随机化 mmap/stack, 2 = 全部随机化(默认)

# 对比两次运行的地址
cat /proc/self/maps | head -5
cat /proc/self/maps | head -5  # 地址不同!

大页内存分配的特殊路径

malloc() 请求的内存 超过 128KB(glibc 默认阈值 M_MMAP_THRESHOLD),不再用 brk() 扩展堆,而是直接调用 mmap() 分配匿名内存页。这意味着:

2. 内存指标详解:VSZ/RSS/PSS/USS/Swap

这是最容易混淆的部分。一张图说清楚:


         进程 A              进程 B
        ┌──────┐            ┌──────┐
VSZ →   │ 300MB│            │ 250MB│    ← 虚拟地址空间总量
        │      │            │      │
        │ ┌────┤            ├────┐ │
RSS →   │ │120M│            │100M│ │    ← 驻留物理内存
        │ │    │            │    │ │
        │ │ ┌──┤            ├──┐ │ │
        │ │ │40│ 共享库libc │40│ │ │    ← 共享部分
        │ │ └──┤            ├──┘ │ │
USS →   │ │80MB│            │60MB│ │    ← 独占物理内存
        │ └────┤            ├────┘ │
        └──────┘            └──────┘

PSS(A) = 80 + 40/2 = 100MB   (共享部分按进程数均分)
PSS(B) = 60 + 40/2 = 80MB

指标对比表

指标全称含义是否计算共享数据来源适用场景
**VSZ / VIRT**Virtual Memory Size虚拟地址空间总大小包含所有映射`ps`, `top`几乎没用 — 包含大量未实际使用的映射
**RSS / RES**Resident Set Size实际驻留在物理 RAM 中的内存共享库被每个进程重复计算`ps`, `top`粗略判断 — 但会高估(共享库重复计)
**PSS**Proportional Set Size按比例分摊共享内存共享部分 ÷ 使用该共享的进程数`/proc/[pid]/smaps`**最准确的单进程内存占用**
**USS**Unique Set Size进程独占的物理内存完全不计共享`/proc/[pid]/smaps`杀掉该进程能释放的内存量
**Swap**Swap Usage被换出到 swap 的内存大小`/proc/[pid]/status`, `smaps`判断进程是否受内存压力影响

实际意义

快速获取各指标


# VSZ 和 RSS (KB)
ps aux | head -1; ps aux | sort -k6 -rn | head -10

# PSS(需要 root 或 smaps_rollup)
sudo cat /proc/$(pgrep -f node)/smaps_rollup | grep Pss

# 一键获取 PSS/USS/RSS
sudo smem -t -k -s pss | head -20

# 单进程完整内存分布
sudo cat /proc/$(pgrep -f node)/smaps | head -60

3. 页面机制

Linux 内存管理的核心单位是 页(page),默认大小 4KB。

3.1 Page Fault(页面错误)

当进程访问一个虚拟地址,而该地址对应的物理页不在 RAM 中时,CPU 触发 page fault 异常,内核介入处理。


进程访问虚拟地址 0x7f3a...
        │
        ▼
   MMU 查页表 ──→ 映射存在且物理页在 RAM?
        │                    │
       否                   是 → 正常访问
        │
        ▼
  触发 Page Fault
        │
        ├─── Minor Fault:页在内存中(页缓存/其他映射),只需更新页表
        │
        ├─── Major Fault:页不在内存中,需要从磁盘读取
        │
        └─── Invalid Fault:非法访问 → SIGSEGV (Segmentation Fault)

Minor Page Fault(次要页面错误)

- 首次访问 mmap() 映射的文件(数据已在 page cache)

- fork() 后子进程首次读取父进程的 COW 页

- Lazy allocation 首次写入

Major Page Fault(主要页面错误)

- 访问 mmap 映射的文件但数据未缓存

- 访问已被 swap out 的页

- 程序首次加载代码页


# 查看进程的 page fault 统计
ps -o pid,min_flt,maj_flt,cmd -p $(pgrep -f node)

# 实时监控
perf stat -e page-faults,minor-faults,major-faults -p <PID> sleep 5

# /proc/[pid]/stat 第 10、12 字段
cat /proc/$(pgrep -f node)/stat | awk '{print "minor:", $10, "major:", $12}'

3.2 Copy-on-Write (COW)

fork() 是 Linux 创建进程的基本方式。如果 fork 时复制整个地址空间,对于一个 RSS 1GB 的进程来说代价太高。

COW 的解决方案:fork 时不复制物理页,父子共享同一份物理内存,把页表标记为只读。当任一方写入时,触发 page fault,内核才复制该页。


fork() 之前:
  父进程 → 物理页 [A][B][C][D]

fork() 之后(COW):
  父进程 ─┐
           ├──→ 物理页 [A][B][C][D]  (共享,标记只读)
  子进程 ─┘

子进程写入页 B:
  父进程 ──→ [A][B ][C][D]
  子进程 ──→ [A][B'][C][D]    ← B 被复制为 B',子进程写 B'

实际影响

3.3 Lazy Allocation(延迟分配)

malloc(100MB) 不会立即分配 100MB 物理内存!


malloc(100MB)
    │
    ▼
内核:好的,在虚拟地址空间标记 100MB 区域(VMA),但不分配物理页
    │
    ▼
VSZ += 100MB, RSS 不变
    │
    ▼
进程写入第 1 页 → page fault → 内核分配 1 个物理页 (4KB)
进程写入第 2 页 → page fault → 内核分配 1 个物理页 (4KB)
...
只有被实际触碰的页才分配物理内存

这就是为什么 VSZ 远大于 RSS。 进程可以"声称"使用大量内存,但操作系统只在实际需要时分配。这也是 Linux overcommit 策略的基础。

4. Linux 内存回收与 OOM Killer

4.1 Overcommit 策略

Linux 默认允许进程申请比物理内存更多的虚拟内存(因为 lazy allocation,申请不等于使用)。但如果所有进程同时使用,物理内存就不够了。

通过 /proc/sys/vm/overcommit_memory 控制:

策略行为适用场景
**0** (默认)Heuristic内核用启发式算法判断。"合理的"过量分配允许,太离谱的拒绝大部分服务器
**1**Always永远允许分配,`malloc()` 永不返回 NULL科学计算、稀疏矩阵(大数组但只用一小部分)
**2**Never严格模式。总 commit ≤ swap + RAM × overcommit_ratio数据库服务器、对 OOM 零容忍的场景

# 查看当前策略
cat /proc/sys/vm/overcommit_memory

# 查看 commit 限制和当前使用
cat /proc/meminfo | grep -i commit
# CommitLimit:    16384000 kB   (overcommit=2 时的上限)
# Committed_AS:    8234567 kB   (当前已承诺的虚拟内存)

overcommit_ratio(仅 overcommit_memory=2 时生效):


cat /proc/sys/vm/overcommit_ratio  # 默认 50
# CommitLimit = Swap + RAM × (overcommit_ratio / 100)
# 例:8GB RAM + 2GB Swap, ratio=50 → CommitLimit = 2 + 8×0.5 = 6GB

4.2 OOM Killer 打分机制

当系统内存耗尽且无法回收时,内核启动 OOM Killer 杀进程以释放内存。

打分算法

每个进程有一个 oom_score(0-1000),分数越高越可能被杀:


基础分 = 进程 RSS / 系统总 RAM × 1000

调整因子:
  + 子进程的 RSS 也计入
  + root 进程得分减半(更不容易被杀)
  + oom_score_adj 手动调整(-1000 ~ 1000)

# 查看进程的 OOM 分数
cat /proc/$(pgrep -f node)/oom_score       # 当前分数
cat /proc/$(pgrep -f node)/oom_score_adj   # 手动调整值

# 保护关键进程(永不被 OOM Kill)
echo -1000 > /proc/$(pgrep -f sshd)/oom_score_adj

# 让某进程优先被杀
echo 1000 > /proc/$(pgrep -f cache-worker)/oom_score_adj

# 在 systemd 中设置
# [Service]
# OOMScoreAdjust=-500

OOM 日志分析


# 查看 OOM Kill 事件
dmesg | grep -i "out of memory"
journalctl -k | grep -i oom

# 典型输出:
# Out of memory: Killed process 12345 (node) total-vm:1234567kB, 
# anon-rss:567890kB, file-rss:12345kB, shmem-rss:0kB, 
# oom_score_adj:0

4.3 内核内存回收路径

在触发 OOM Killer 之前,内核会先尝试回收内存:


内存不足
    │
    ▼
1. 回收 Page Cache(文件缓存)
    │ ─ 干净页直接丢弃
    │ ─ 脏页写回磁盘后丢弃
    │
    ▼ 仍不够
2. 回收 Slab Cache(内核对象缓存)
    │ ─ dentry cache, inode cache
    │
    ▼ 仍不够
3. Swap Out(将匿名页写入 swap)
    │ ─ 受 swappiness 控制
    │
    ▼ 仍不够
4. 触发 OOM Killer
    │ ─ 选分数最高的进程杀掉
    └── 释放其内存

5. Swap 机制

5.1 Swap 是什么

Swap 是磁盘上的空间,充当物理 RAM 的"溢出区"。当物理内存紧张时,内核将不活跃的内存页写入 swap(swap out),需要时再读回来(swap in)。

常见误解

5.2 swappiness 参数

控制内核在回收内存时倾向于回收 page cache 还是 swap 匿名页:


cat /proc/sys/vm/swappiness  # 默认 60

# 范围 0-200(内核 5.8+,之前是 0-100)
# 0   = 尽量不 swap,优先回收 page cache(但极端情况仍会 swap)
# 60  = 默认,平衡策略
# 100 = page cache 和 swap 同等对待
# 200 = 激进 swap(cgroup v2 中可用)

推荐设置


# 临时修改
sudo sysctl vm.swappiness=10

# 永久修改
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.d/99-swap.conf
sudo sysctl -p /etc/sysctl.d/99-swap.conf

5.3 创建 Swap File

现代 Linux 推荐用 swap file 而不是 swap 分区(更灵活,随时可加减):


# 创建 4GB swap 文件
sudo fallocate -l 4G /swapfile
# 如果 fallocate 不支持(btrfs),用 dd:
# sudo dd if=/dev/zero of=/swapfile bs=1M count=4096

# 设置权限(必须 600,否则不安全)
sudo chmod 600 /swapfile

# 格式化为 swap
sudo mkswap /swapfile

# 启用
sudo swapon /swapfile

# 验证
swapon --show
free -h

# 开机自动挂载 — 加入 /etc/fstab
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 调整 swap 大小(增加)
sudo swapoff /swapfile
sudo fallocate -l 8G /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 完全移除 swap
sudo swapoff /swapfile
sudo rm /swapfile
# 并从 /etc/fstab 删除对应行

6. 实用诊断命令

6.1 ps — 进程内存概览


# 按 RSS 排序,显示前 15 个进程
ps aux --sort=-%mem | head -15

# 自定义格式
ps -eo pid,user,%mem,rss,vsz,comm --sort=-%mem | head -15

# 特定进程
ps -o pid,rss,vsz,%mem,cmd -p $(pgrep -f "node\|python\|java")

6.2 pmap — 进程内存映射


# 基本映射
pmap <PID>

# 详细模式(显示每个映射区的脏页、共享/私有等)
pmap -x <PID>

# 超详细模式
pmap -XX <PID>

# 实际用法:看 node 进程的内存映射
pmap -x $(pgrep -f node) | tail -5
# 最后一行是汇总:total   VSZ   RSS   Dirty

输出示例


Address           Kbytes     RSS   Dirty Mode  Mapping
0000555555554000    1024     512     0   r-x-- node            ← 代码段
0000555555754000      16      16    16   rw--- node            ← 数据段
00007f3a00000000  262144  131072 131072  rw---   [ anon ]      ← V8 堆
00007f3a8c000000   16384    8192    0   r-x-- libc.so.6       ← 共享库
...

6.3 smaps — 最精确的内存分析


# 完整的内存映射详情
sudo cat /proc/<PID>/smaps

# 汇总版(内核 4.14+)
sudo cat /proc/<PID>/smaps_rollup

# 输出关键字段:
# Size:          映射大小(≈ VMA 大小)
# Rss:           驻留物理内存
# Pss:           按比例分摊的物理内存
# Private_Clean: 私有干净页(可直接丢弃)
# Private_Dirty: 私有脏页(USS 的主要组成)
# Shared_Clean:  共享干净页
# Shared_Dirty:  共享脏页
# Swap:          被换出的大小
# SwapPss:       按比例分摊的 swap

# 一键提取关键指标
sudo awk '/^Rss/{rss+=$2} /^Pss/{pss+=$2} /^Private_Dirty/{pd+=$2} /^Swap/{sw+=$2} 
  END{printf "RSS: %d MB\nPSS: %d MB\nUSS(Private_Dirty): %d MB\nSwap: %d MB\n", 
  rss/1024, pss/1024, pd/1024, sw/1024}' /proc/<PID>/smaps

6.4 /proc/meminfo — 系统级内存总览


cat /proc/meminfo

# 关键字段解释:
# MemTotal:        物理总内存
# MemFree:         完全空闲(未被任何用途使用)
# MemAvailable:    可用内存 = Free + 可回收的 Cache/Buffer
# Buffers:         块设备 I/O 缓冲
# Cached:          Page Cache(文件内容缓存)
# SwapTotal:       Swap 总大小
# SwapFree:        Swap 空闲
# Dirty:           待写回磁盘的脏页
# Slab:            内核 slab 分配器使用
# SReclaimable:    可回收的 slab
# CommitLimit:     overcommit=2 时的限制
# Committed_AS:    已承诺的虚拟内存总量

⚠️ 常见误区


# 快速查看:真正可用内存
free -h
#               total        used        free      shared  buff/cache   available
# Mem:           15Gi       6.2Gi       512Mi       256Mi       8.8Gi       8.5Gi
#                                       ^^^^                                ^^^^^
#                                     别看这个                           看这个!

6.5 cgroup — 容器/服务级内存限制

在容器化和 systemd 时代,cgroup 是管理进程内存的标准方式。

cgroup v2(现代 Linux 默认)


# 查看 cgroup 内存限制
cat /sys/fs/cgroup/<cgroup-path>/memory.max         # 硬限制
cat /sys/fs/cgroup/<cgroup-path>/memory.high        # 软限制(超过后内核加速回收)
cat /sys/fs/cgroup/<cgroup-path>/memory.current     # 当前使用

# 查看 OOM 事件
cat /sys/fs/cgroup/<cgroup-path>/memory.events
# low 0         ← 触发 memory.low 保护的次数
# high 5        ← 超过 memory.high 的次数
# max 0         ← 达到 memory.max 的次数
# oom 0         ← 触发 OOM 的次数
# oom_kill 0    ← 被 OOM Kill 的次数

Docker 容器内存


# 启动时限制内存
docker run -m 512m --memory-swap 1g myapp

# 查看容器内存使用
docker stats <container>

# 查看 cgroup 详情
docker inspect <container> | jq '.[0].HostConfig.Memory'
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.current

systemd 服务内存限制


# /etc/systemd/system/myapp.service
[Service]
MemoryMax=512M          # 硬限制(超过触发 OOM Kill)
MemoryHigh=400M         # 软限制(超过后内核积极回收)
MemorySwapMax=0         # 禁用 swap
OOMScoreAdjust=-500     # 降低被系统级 OOM Kill 的概率

6.6 其他有用工具


# vmstat — 内存 + swap 活动
vmstat 1 5
# 关注 si(swap in)和 so(swap out),持续非零说明 swap thrashing

# smem — PSS/USS 排序
sudo smem -t -k -s pss

# htop — 交互式进程查看器
# 按 Shift+M 按内存排序
# Setup → Columns 可以添加 VIRT/RES/SHR

# valgrind — 内存泄漏检测(开发时用)
valgrind --leak-check=full ./myapp

# perf — 内存相关性能分析
perf record -e page-faults -g -p <PID> sleep 10
perf report

7. Node.js 特有的内存知识

7.1 V8 Heap 架构

Node.js 使用 V8 引擎,有自己独立的内存管理层:


Node.js 进程内存
├── V8 Heap(JS 对象,受 V8 GC 管理)
│   ├── New Space (Young Generation) ~ 1-8MB
│   │   ├── Semi-space A (From)
│   │   └── Semi-space B (To)          ← Scavenge GC(频繁,< 1ms)
│   ├── Old Space ~ 默认上限 ~1.7GB
│   │   ├── Old Pointer Space(含指针的对象)
│   │   └── Old Data Space(纯数据对象)  ← Mark-Sweep-Compact GC
│   ├── Large Object Space(> 256KB 的大对象)
│   ├── Code Space(JIT 编译的机器码)
│   └── Map Space(Hidden Class / 对象形状信息)
├── V8 外部内存(ArrayBuffer、TypedArray 的底层 buffer)
├── C++ 堆(libuv、native addons)
├── 线程栈(主线程 + worker threads + libuv 线程池)
└── 共享库(libc、libstdc++、libssl...)

7.2 关键参数


# 查看默认内存限制
node -e "console.log(v8.getHeapStatistics())"

# 设置 Old Space 上限
node --max-old-space-size=4096 app.js   # 4GB

# 设置 New Space 大小(每个 semi-space)
node --max-semi-space-size=64 app.js    # 64MB

# 在 NODE_OPTIONS 中设置(推荐用于生产环境)
export NODE_OPTIONS="--max-old-space-size=4096"

默认限制(V8 自动根据可用内存调整,近似值):

系统内存Old Space 默认上限
< 2GB~512MB
2-4GB~1GB
> 4GB~1.7GB
64 位,8GB+~2GB(某些 Node 版本更高)

7.3 RSS 只涨不缩现象

这是 Node.js 开发者最困惑的问题之一:RSS 持续增长,即使 V8 Heap 已经 GC 释放了内存


V8 Heap:  ████████░░░░░░░░  (500MB used / 1GB allocated)
RSS:      ██████████████████  (1.8GB,比 V8 Heap 大很多,而且不降)

原因分析

1. glibc malloc 不归还内存:V8 通过 malloc() 从操作系统获取内存。GC 释放对象后,V8 调用 free(),但 glibc 的 ptmalloc2 倾向于缓存释放的内存(放入 free list),不调用 brk()/munmap() 归还给 OS。

2. 内存碎片化:堆中间有少量存活对象,整个区域就无法归还。

3. V8 自身保留:V8 维护多个 memory space,GC 后保留一定的预分配空间,不会立即缩容。

4. 外部内存:Buffer、TypedArray 的底层 C++ 内存不受 V8 Heap 统计,但计入 RSS。

实际影响


// 监控 Node.js 内存
setInterval(() => {
  const mem = process.memoryUsage();
  console.log({
    rss: (mem.rss / 1024 / 1024).toFixed(1) + 'MB',
    heapTotal: (mem.heapTotal / 1024 / 1024).toFixed(1) + 'MB',
    heapUsed: (mem.heapUsed / 1024 / 1024).toFixed(1) + 'MB',
    external: (mem.external / 1024 / 1024).toFixed(1) + 'MB',
    arrayBuffers: (mem.arrayBuffers / 1024 / 1024).toFixed(1) + 'MB',
  });
}, 10000);

7.4 用 jemalloc 替换 glibc malloc

解决 RSS 不降的有效方案:用 jemalloc 替代 glibc ptmalloc2。jemalloc 更积极地将空闲内存归还 OS。


# 安装 jemalloc
sudo apt install libjemalloc2

# 用 LD_PRELOAD 替换(无需重新编译 Node.js)
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 node app.js

# 或在 systemd service 中
# [Service]
# Environment=LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2

7.5 Node.js 内存泄漏排查


// 方法1: 生成 heap snapshot
const v8 = require('v8');
const fs = require('fs');

// 手动触发
v8.writeHeapSnapshot();  // 生成 .heapsnapshot 文件

// 方法2: 通过 --inspect 远程调试
// node --inspect app.js
// Chrome DevTools → Memory → Take Heap Snapshot

// 方法3: 用 clinic.js
// npx clinic doctor -- node app.js
// npx clinic heap -- node app.js

常见泄漏模式

模式示例排查方法
全局缓存无限增长`const cache = {}; cache[key] = data;`Heap snapshot 对比,找增长最快的对象类型
事件监听器未移除`emitter.on(...)` 但忘了 `off(...)``process._getActiveHandles().length`
闭包意外捕获定时器闭包持有大对象引用Heap snapshot 查看 retainers
Stream 未消费可读流数据堆积在内部 buffer检查 `stream.readableLength`

总结速查表

问题看什么命令
进程用了多少内存?PSS`sudo smem -k -s pss` 或 `smaps_rollup`
杀掉它能释放多少?USS`smaps` → `Private_Dirty + Private_Clean`
系统还有多少可用内存?MemAvailable`free -h` → available 列
为什么被 OOM Kill?oom_score + dmesg`dmesg \grep oom`
进程在频繁 swap?si/so`vmstat 1`
Node.js 是否泄漏?heapUsed 趋势`process.memoryUsage()` 定时打印
容器内存限制?cgroup`memory.max` / `memory.current`

参考资料