Linux 内存管理与性能优化:从虚拟内存、Page Cache 到 Swap、OOM 与泄漏排查

Linux 内存管理与性能优化:从虚拟内存、Page Cache 到 Swap、OOM 与泄漏排查

Linux 内存性能问题很容易陷入两个极端:一边是只会看 freetop,看到 free 很小就认定“内存不够”;另一边是一下子钻进页表、LRU、NUMA、Swap、Slab、OOM 的细节,知道很多名词,却不知道线上出问题时该先看什么。

真正有用的知识结构应该把几条链路串起来:

  • 进程看到的是虚拟地址,真正使用的是物理页。
  • 内存分配并不等于物理内存立刻到手,首次访问才可能触发缺页并分配物理页。
  • Linux 会把空闲内存积极用于缓存,因此 free 很低并不等价于内存紧张。
  • 文件页、匿名页的回收方式不同,进而引出 Page Cache、Swap、LRU、OOM。
  • 性能分析不能只看一个瞬时值,而要从系统整体、变化趋势、具体进程一路缩小范围。

把这些关系建立起来以后,freevmstatsarpmapsmapscachetopmemleak 等工具就不再是一串需要死记硬背的命令,而是对应内存工作机制的不同观察窗口。

1. 先建立一张 Linux 内存全景图

从应用程序的一次内存访问开始,可以把 Linux 内存系统理解为下面这条链路:

flowchart LR
    A[进程访问虚拟地址] --> B{TLB 是否命中}
    B -->|命中| C[得到物理页映射]
    B -->|未命中| D[MMU 查页表]
    D --> E{页表中是否已有有效映射}
    E -->|有| C
    E -->|没有| F[缺页异常 Page Fault]
    F --> G[内核处理缺页]
    G --> H{页面来源}
    H -->|匿名页| I[分配/初始化物理页]
    H -->|文件页| J[从文件或 Page Cache 获取]
    H -->|被换出的匿名页| K[从 Swap 换入]
    I --> L[更新页表]
    J --> L
    K --> L
    L --> C
    C --> M[CPU 访问物理内存]

这张图解释了后面几乎所有内存性能指标:

  • TLB 命中率不好,会增加地址翻译开销;
  • 页表中没有映射,会发生缺页;
  • 如果缺页只需要分配一个物理页,通常属于 Minor Page Fault(次缺页)
  • 如果需要从磁盘、Swap 等慢速介质读取数据,则会形成 Major Page Fault(主缺页)
  • 如果物理内存不足,内核会尝试回收文件页、换出匿名页;
  • 回收仍无法满足分配请求时,才会走向 OOM。

所以 Linux 内存性能分析,本质上是在回答四个问题:

  1. 内存被谁占了?
  2. 这些内存是什么类型?
  3. 它们是否可以被回收?
  4. 系统是否因为回收、缺页、Swap 或 OOM 付出了额外代价?

2. 物理内存与虚拟内存:进程看到的并不是 DRAM

通常说服务器有 8 GB、32 GB、128 GB 内存,指的是 Physical Memory(物理内存),常见实现是 DRAM。

应用进程并不直接拿物理地址读写 DRAM。Linux 为每个进程提供一个独立的 Virtual Address Space(虚拟地址空间)。进程看到的是连续的虚拟地址,至于这些地址到底对应物理内存中的哪一页,由内核建立映射关系。

这样做带来了几个关键能力:

  • 每个进程拥有独立地址空间,进程之间天然隔离;
  • 虚拟地址可以远大于实际物理内存;
  • 物理内存可以按需分配,而不是进程一申请虚拟地址就全部兑现;
  • 同一份物理页面可以映射到多个进程,实现共享库、共享内存等;
  • 内核可以通过页权限实现只读、可写、可执行等访问控制。

2.1 用户空间与内核空间

虚拟地址空间通常被划分为:

  • User Space(用户空间)
  • Kernel Space(内核空间)

进程在用户态只能访问用户空间;进入内核态后,才能执行需要内核权限的操作。

资料使用 32 位系统的经典 3G 用户空间 + 1G 内核空间,以及 64 位系统中用户空间、内核空间各 128 TB 的示意来帮助理解。需要注意,这些数字是特定架构和当时实现下的经典示例,不是所有现代 64 位 Linux 的固定布局。实际可用虚拟地址宽度、页表级数、内核配置和体系结构都会影响地址空间布局。

真正应该记住的是:

地址空间布局会随架构和内核变化,但“每个进程拥有独立虚拟地址空间,用户态与内核态权限不同”这个模型不变。


3. 页、页表、MMU 与 TLB

3.1 内存不是按字节做映射,而是按页

Linux 虚拟内存管理的基本单位是 Page(页)。常见普通页大小为 4 KB。

如果使用 4 KB 页:

1
4 GB / 4 KB = 1,048,576 个页

如果为每个虚拟页都保存一条映射,页表会非常庞大。因此 Linux 使用 Multi-level Page Table(多级页表),只为实际使用到的地址空间建立必要的页表层级。

资料以经典四级页表说明虚拟地址可以拆成:

1
PGD -> PUD -> PMD -> PTE -> Offset

现代体系结构上页表级数可能更多,例如某些 x86_64 系统支持五级页表。对性能分析来说,重点不是背层级名称,而是理解:

  • 虚拟地址会被拆成若干级索引;
  • 最终定位到 PTE;
  • PTE 再给出物理页信息和访问权限;
  • 页内偏移不需要再次翻译。

3.2 页表到底放在哪里?

资料正文中有一句容易误导的简化描述:“页表存储在 MMU 中”。后续答疑已经把这个问题讲得更准确:

  • 页表本身是内核维护的数据结构,存放在内存中;
  • MMU(Memory Management Unit,内存管理单元)是执行虚拟地址到物理地址翻译的硬件;
  • TLB(Translation Lookaside Buffer,转译后备缓冲器)缓存常用的地址翻译结果。

可以理解为:

flowchart LR
    VA[Virtual Address] --> TLB{TLB}
    TLB -->|Hit| PA[Physical Address]
    TLB -->|Miss| MMU[MMU Page Walk]
    PT[(Page Tables in Memory)] --> MMU
    MMU --> PA
    MMU --> TLB

TLB 的意义非常直接:如果每次内存访问都要在内存里逐级查页表,代价会非常高。TLB 把最近使用的地址翻译缓存下来,避免重复页表遍历。

这也解释了为什么上下文切换、页表规模以及大页都会影响内存访问性能。


4. HugePage 为什么能提升大内存应用性能

普通 4 KB 页意味着大内存应用需要维护大量页表项,也更容易给 TLB 带来压力。

Linux 支持更大的页面,例如资料中提到的:

  • 2 MB
  • 1 GB

大页常见于:

  • 数据库;
  • DPDK;
  • 大型缓存;
  • 大内存计算;
  • 对 TLB Miss 敏感的场景。

大页的主要收益包括:

  1. 减少页表项数量;
  2. 提高单个 TLB Entry 覆盖的内存范围;
  3. 降低地址翻译开销。

但大页并不是“打开就一定更快”。它会带来更粗的内存分配粒度,可能增加内存浪费,也会改变内存回收和碎片管理方式。因此应当基于负载特征使用,而不是把 HugePage 当成通用开关。


5. 一个 Linux 进程的虚拟地址空间怎么分布

以经典布局理解,一个用户进程通常会包含:

区域 主要内容 典型特征
只读段 程序代码、常量 通常只读,可被多个进程共享
数据段 全局变量、静态变量 生命周期通常与进程一致
Heap(堆) 动态分配内存 由应用/分配器管理
Memory Mapping Area(映射区) 动态库、文件映射、共享内存、匿名映射 常由 mmap() 建立
Stack(栈) 局部变量、函数调用上下文 自动分配和回收

资料用“堆从低地址向高地址增长、文件映射区从高地址向低地址增长”的经典图帮助理解。

需要注意,现代系统启用了 ASLR(地址空间布局随机化)后,具体地址位置并不会像教材图那样固定。栈大小也不是永远固定为 8 MB,而是受 ulimit、线程实现和系统配置影响。

查看当前 Shell 的栈限制可以使用:

1
ulimit -s

6. malloc、brk、mmap:申请虚拟内存不等于立刻占用物理内存

C 程序中最常见的动态内存入口是:

1
void *p = malloc(size);

malloc() 是 C 标准库接口,不是一个单独的内核系统调用。分配器会根据内存大小、空闲块情况和实现策略选择不同机制。

资料用经典 glibc 策略解释:

  • 较小内存常通过 brk() 扩展堆;
  • 较大内存常通过 mmap() 建立匿名映射;
  • 示例阈值为约 128 KB。

这个 128 KB 适合用来理解机制,但不要当成固定 ABI。实际阈值会受 glibc 版本、分配器策略和运行时状态影响。

6.1 brk 的特点

brk() 通过移动 Program Break 扩展堆。

优点:

  • 已经向进程堆申请过的区域可以被用户态分配器重复利用;
  • 可以减少频繁系统调用;
  • 可以降低反复建立映射、反复缺页带来的开销。

代价:

  • 释放后的内存未必马上归还内核;
  • 高地址仍被占用时,低地址释放的小块很可能只回到分配器的空闲链表;
  • 长期频繁分配、释放可能形成碎片。

6.2 mmap 的特点

mmap() 可以在映射区建立一段新的虚拟内存区域。

优点:

  • 大块内存释放后更容易直接解除映射并归还系统;
  • 单独的映射便于管理。

代价:

  • 建立和销毁映射需要更多内核参与;
  • 首次访问仍然可能触发缺页;
  • 高频大规模 mmap()/munmap() 会增加 VMA 和页表管理成本。

6.3 First Touch:真正的物理页往往在第一次访问时才分配

下面的理解非常关键:

1
void *p = malloc(1024 * 1024 * 1024);

并不意味着内核立即拿出 1 GB 物理内存交给进程。

通常过程是:

sequenceDiagram
    participant App as Application
    participant Alloc as malloc allocator
    participant Kernel as Linux Kernel
    participant MM as Physical Memory

    App->>Alloc: malloc(1GB)
    Alloc->>Kernel: brk/mmap 建立虚拟地址区间
    Kernel-->>Alloc: 返回虚拟地址
    Alloc-->>App: 返回指针

    App->>App: 首次写入某个页面
    App->>Kernel: Page Fault
    Kernel->>MM: 分配物理页
    Kernel->>Kernel: 更新页表
    Kernel-->>App: 恢复执行

这就是为什么:

  • VIRT/VSS 很大,不代表物理内存真的用了那么多;
  • 大量 malloc() 后不触碰内存,RSS 可能变化不大;
  • 真正“写满”内存时,问题才会逐渐暴露。

7. 内核如何分配物理内存:Buddy 与 Slab

物理页层面的分配,Linux 主要使用 Buddy System(伙伴系统) 管理连续页块。

伙伴系统通过按 2 的幂次维护不同阶的连续页块,在分配和释放时拆分、合并相邻块,以降低外部碎片。

但内核里有大量对象远小于一个页面,例如:

  • inode;
  • dentry;
  • task_struct;
  • socket 相关结构;
  • 各种内核对象。

如果每个小对象都独占 4 KB 页面,浪费会非常严重。因此内核在伙伴系统之上构建了 Slab 类分配器,用于高效缓存和复用小对象。

资料使用 slab 作为统称。现代 Linux 中常见的是 SLUB 实现,但性能分析时仍经常使用 Slab 这一概念和 /proc/slabinfoslabtop 这些接口。

这里必须区分一个常见误解:

用户空间的 malloc() 不会直接使用内核 Slab 来管理用户对象。Slab 主要用于内核空间小对象。

用户空间分配器向内核申请页或映射,再在用户空间内部做更细粒度的切分和复用。


8. 怎么正确读 free:free 小不等于内存紧张

最常见的系统内存入口是:

1
free -h

典型指标包括:

指标 含义
total 总物理内存
used 已使用内存
free 当前完全未使用的内存
shared 共享内存相关使用量
buff/cache Buffer、Page Cache、可回收 Slab 等缓存
available 内核估计的新进程在不发生明显 Swap 的情况下可用的内存

真正需要重点观察的不是单独的 free,而是:

1
available

Linux 的设计倾向是:

与其让物理内存闲着,不如拿来做缓存。

所以一个运行稳定的 Linux 服务器,free 很小、buff/cache 很大,完全可能是健康状态。

判断内存压力时应该结合:

  • available
  • Swap 是否活跃;
  • vmstatsi/so
  • Major Fault 是否升高;
  • 回收线程是否频繁活动;
  • 应用延迟是否与内存回收同步恶化。

9. 进程内存指标:VIRT、RES/RSS、SHR、PSS 到底怎么看

9.1 VIRT / VSS

VIRTVSS 表示进程拥有的虚拟地址空间,可能包括:

  • 代码;
  • 数据;
  • 堆;
  • 映射文件;
  • 动态库;
  • 共享内存;
  • 已申请但尚未真正映射物理页的区域;
  • 已经换出到 Swap 的页面。

因此:

VIRT 很大本身并不说明内存泄漏,也不说明机器快要 OOM。

9.2 RES / RSS

RES 常对应 Resident Set Size(RSS),表示当前驻留在物理内存中的页面规模。

资料前文曾把 RES/RSS 简化描述成“不包括共享内存”,但后续答疑明确指出:工具给出的 RSS/RES 与这个简化概念存在出入,实践必须以具体工具定义为准。

对常见 Linux 工具来说,RSS/RES 通常会把当前驻留的共享页也算进来,因此不能简单把多个进程的 RSS 相加得到“所有进程真正独占的物理内存”。

9.3 SHR

SHR 是可共享的驻留内存的一部分,可能包括:

  • 真正的共享内存;
  • 共享库;
  • 程序代码页;
  • 其他可共享映射。

SHR 并不代表“这部分一定正在被别的进程共享”。

9.4 PSS

**PSS(Proportional Set Size,按比例分摊的常驻集)**是统计多进程实际内存归属时更有意义的指标。

假设:

  • 进程独占 1000 页;
  • 另外 1000 页与另一个进程共享;

那么:

1
PSS = 1000 + 1000 / 2 = 1500 页

把所有进程 PSS 相加,可以避免共享页被重复统计。

例如:

1
2
grep Pss /proc/[1-9]*/smaps \
| awk '{total += $2} END {printf "%d kB\n", total}'

如果要深入看单个进程:

1
cat /proc/<pid>/smaps

或者:

1
pmap -x <pid>

10. Page Fault:内存慢不一定是“内存带宽慢”

缺页异常是 Linux 内存分析中经常被忽略、但非常重要的一组指标。

Minor Page Fault

页面尚未建立当前进程可用的映射,但数据已经在内存中,或者只需要分配/建立映射,不需要磁盘读取。

典型场景:

  • First Touch;
  • Copy-on-Write;
  • 已经在 Page Cache 中的文件页建立进程映射。

Major Page Fault

需要慢速 I/O 才能把页面带回内存。

典型场景:

  • 页面已经被换出到 Swap;
  • 文件页需要从存储设备重新读取。

因此当 Major Fault 持续增加时,真正的瓶颈往往已经跨越“内存”边界,进入磁盘 I/O。


11. Buffer 与 Cache:两个最容易被误解的指标

freebuff/cache 实际包含多个来源。

资料从 /proc/meminfoman proc 出发,梳理了三个关键值:

  • Buffers
  • Cached
  • SReclaimable

可以把它们理解为:

指标 核心含义
Buffers 块设备相关的原始磁盘块缓存
Cached 文件页缓存 Page Cache
SReclaimable Slab 中可以在内存压力下回收的部分

很多资料会把 Buffer 简化成“写缓存”、Cache 简化成“读缓存”,但实验表明这种说法太粗糙。

更准确的工程理解是:

Buffer 更偏向块设备层的数据缓存;Cache 更偏向文件系统管理的文件页缓存。它们都可能参与读,也都可能参与写。


12. 普通文件、块设备、裸 I/O、Direct I/O 不是一回事

这是理解 Buffer、Page Cache、O_DIRECT 的关键。

12.1 普通文件 I/O

假设:

1
dd if=/tmp/file of=/dev/null

/tmp/file 是文件系统中的普通文件,I/O 路径大体是:

flowchart LR
    A[Application] --> B[VFS]
    B --> C[File System]
    C --> D[Page Cache]
    D --> E[Block Layer]
    E --> F[Device]

文件读写会使用 Page Cache。

12.2 块设备 I/O,也就是资料中的“裸 I/O”

例如:

1
dd if=/dev/sda1 of=/dev/null

这里直接读块设备文件,不通过某个普通文件的文件系统语义。

概念上可以理解为:

flowchart LR
    A[Application] --> B[Block Device]
    B --> C[Block Layer]
    C --> D[Device]

资料把这种“跳过文件系统、直接访问块设备”的方式称为 Raw I/O(裸 I/O)

12.3 Direct I/O

O_DIRECT 是另外一件事。

它的目标是尽量绕过 Page Cache,让应用缓冲区和存储设备之间直接进行 I/O,减少双重缓存和拷贝。

所以:

裸 I/O强调“是否经过文件系统”;Direct I/O强调“是否绕过系统页缓存”。

二者不能混为一谈。


13. 文件读写为什么也会让 Cache 增长

资料通过四组 dd + vmstat 实验观察了 Buffer/Cache:

场景 主要现象
写普通文件 Cache 明显增长
写块设备 Buffer 明显增长
读普通文件 Cache 明显增长
读块设备 Buffer 明显增长

这说明 Page Cache 并不只是“读缓存”。

写普通文件

典型流程是:

sequenceDiagram
    participant App
    participant Cache as Page Cache
    participant Kernel as Kernel Writeback
    participant Disk

    App->>Cache: write()
    Cache-->>App: 很快返回
    Note over Cache: 页面被标记为 Dirty
    Kernel->>Disk: 后台回写脏页

应用不必每次 write() 都等待磁盘真正完成落盘,因此吞吐和响应时间都可以明显改善。

读普通文件

第一次读:

1
Disk -> Page Cache -> Application

后续重复读:

1
Page Cache -> Application

这就是为什么同一个文件第二次读往往快很多。


14. drop_caches 适合实验,不适合当“优化命令”

资料为了减少实验干扰使用:

1
echo 3 > /proc/sys/vm/drop_caches

它会释放可以回收的 Page Cache、dentry、inode 等缓存。

这个命令非常适合:

  • 做缓存冷启动实验;
  • 对比第一次读取与第二次读取;
  • 验证某个现象是否与系统缓存有关。

但不应该把它当成常规“释放内存优化”。

原因很简单:

你主动清掉的,正是 Linux 为了加速后续 I/O 而保留的缓存。

如果每次看到 Cache 高就 drop_caches,相当于把操作系统主动做好的性能优化扔掉,然后让下一个请求重新读盘。


15. 系统缓存到底有没有用:用命中率说话

缓存是否有效,不能只看“缓存占了多少内存”,更应该看 Cache Hit Ratio(缓存命中率)

基本公式:

1
Cache Hit Ratio = Cache Hits / Total Requests

命中率越高,说明更多请求可以在内存中完成,减少慢速磁盘 I/O。

资料使用 BCC 中的:

  • cachestat
  • cachetop

来观察缓存。

15.1 cachestat

用于看系统整体缓存读写情况,典型字段包括:

字段 含义
TOTAL 总 I/O 次数
MISSES 缓存未命中
HITS 缓存命中
DIRTIES 新产生的脏页
BUFFERS_MB Buffer 大小
CACHED_MB Cache 大小

示例:

1
cachestat 1 3

15.2 cachetop

用于按进程观察缓存命中。

1
cachetop

常见字段:

  • HITS
  • MISSES
  • DIRTIES
  • READ_HIT%
  • WRITE_HIT%

15.3 pcstat

pcstat 可以查看某个具体文件有多少页面已经在 Page Cache 中

例如:

1
pcstat /bin/ls

这比“整个系统 Cache 有多大”更接近某个文件的实际缓存状态。

资料里的 BCC 和 pcstat 安装步骤来自 2018-2019 年环境,仓库地址、包名和安装方式具有明显时代性。真正使用时应优先查当前发行版和项目文档,而不是照抄旧安装命令。


16. 一个非常典型的缓存实验:为什么第二次 dd 能从 33 MB/s 变成 4.5 GB/s

资料先构造一个 512 MB 文件,然后清理缓存:

1
2
dd if=/dev/sda1 of=file bs=1M count=512
echo 3 > /proc/sys/vm/drop_caches

确认文件不在缓存:

1
pcstat file

第一次读取:

1
dd if=file of=/dev/null bs=1M

示例结果约:

1
33.4 MB/s

第二次读取同一文件:

1
dd if=file of=/dev/null bs=1M

示例结果约:

1
4.5 GB/s

这当然不是磁盘突然变成了 4.5 GB/s。

第二次读已经基本命中 Page Cache,测到的是:

  • 内存访问;
  • 内核路径;
  • 数据拷贝;

而不再是存储设备的真实吞吐。

这个案例直接给出一条性能测试原则:

测磁盘性能之前,必须先明确你要测“存储设备本身”,还是“文件系统 + Page Cache 的实际应用性能”。

否则同一个 dd 命令可以测出完全不同的东西。


17. O_DIRECT:缓存命中率很高,为什么应用还是读得很慢

资料中另一个案例每次读取约 32 MB,但耗时约 0.9 秒。

cachetop 看上去命中率是 100%,似乎应该很快,可根据页面数换算后发现缓存吞吐只有很小一部分,和应用实际读取量不一致。

进一步通过 strace

1
strace -p $(pgrep app)

观察到:

1
openat(..., "/dev/sdb1", O_RDONLY|O_DIRECT) = 4

关键就是:

1
O_DIRECT

源代码类似:

1
2
int flags = O_RDONLY | O_LARGEFILE | O_DIRECT;
int fd = open(disk, flags, 0755);

删除 O_DIRECT 后,读取 32 MB 的时间从接近 0.9 秒下降到约 0.03 秒量级。

这个案例揭示了两个非常重要的排查经验。

第一,性能工具的“100%”并不等于你以为的 100%

cachetop 并不会把所有 Direct I/O 数据完整纳入同一种 Page Cache 统计逻辑。

所以看到:

1
READ_HIT% = 100%

不能立刻得出“应用全部从缓存读取”。

还必须对照:

  • 应用实际读了多少字节;
  • 命中了多少页;
  • 每页多大;
  • 统计窗口多长。

如果指标之间算不平,就说明还有另一条 I/O 路径。

第二,性能分析最终要能落到系统调用

当现象和指标矛盾时:

1
2
3
cachetop -> 发现缓存数据量对不上
strace -> 找到 openat(... O_DIRECT)
source -> 确认代码主动绕过缓存

这比只说“磁盘慢”更接近真正的根因。


18. 内存泄漏:最重要的不是“内存高”,而是“不可解释地持续增长”

Memory Leak(内存泄漏)不是简单的“进程占用内存很多”。

更有意义的定义是:

程序已经不再需要某些动态内存,却仍然持有它们,导致这些内存无法再次利用或回收。

典型危害链路:

flowchart LR
    A[内存泄漏持续累积] --> B[Available 内存下降]
    B --> C[回收 Page Cache]
    C --> D[I/O 增加]
    B --> E[Swap 增加]
    E --> F[Major Fault 增加]
    F --> G[请求延迟升高]
    B --> H[回收仍不足]
    H --> I[OOM]

所以内存泄漏最终很可能表现为:

  • 缓存突然变少;
  • Swap 开始活跃;
  • I/O 增加;
  • 延迟抖动;
  • OOM;
  • 其他原本正常的进程也被拖慢。

19. 哪些内存最容易泄漏

栈内存

局部变量通常在作用域退出时自动回收。

例如:

1
2
3
void f(void) {
int data[64];
}

函数结束后,栈帧被回收,不需要手动 free()

堆内存

通过:

1
2
3
malloc()
calloc()
realloc()

等方式获得的动态内存,需要匹配:

1
free()

否则最容易形成经典堆内存泄漏。

文件映射和共享内存

mmap()、共享内存等映射资源同样需要正确解除。

典型接口:

1
munmap(addr, length);

所以从进程地址空间看,最值得重点盯住的是:

  • Heap;
  • Anonymous Mapping;
  • Shared Memory;
  • 长期增长的映射区。

20. 怎么定位内存泄漏:先看趋势,再找调用栈

资料给出了一条非常实用的路径。

第一步:确认系统内存在持续下降

1
vmstat 3

如果持续看到:

  • free 下降;
  • buff/cache 相对稳定;
  • Swap 尚未明显介入;

说明某类不可回收或暂时不能回收的内存正在增加。

但这仍然不能直接证明泄漏,因为业务可能本来就在建立一个合法的大缓存。

所以重点是“趋势 + 业务预期”。

第二步:定位增长最快的进程

可以用:

1
top

M 按内存排序。

也可以:

1
ps aux --sort=-rss | head

或者:

1
pidstat -r 1

第三步:查看进程地址空间

1
pmap -x <pid>

以及:

1
cat /proc/<pid>/smaps

观察到底是:

  • heap;
  • anonymous mapping;
  • file mapping;
  • shared memory;

哪一部分在增长。

第四步:追踪“分配了但没有释放”的调用栈

资料使用 BCC 的 memleak

1
/usr/share/bcc/tools/memleak -a -p <pid>

它可以跟踪:

  • 内存分配;
  • 内存释放;
  • Outstanding Allocation;
  • 对应调用栈。

如果生产环境内核太旧,资料还给出了一个重要替代思路:

1
Valgrind

也就是说:

工具不是核心,核心是“找到仍未释放的分配记录,并映射回代码调用栈”。


21. 一个最小内存泄漏案例

资料中的 Fibonacci 示例,为了让泄漏更明显,每次分配 1024 个 long long

1
2
3
4
5
6
long long *fibonacci(long long *n0, long long *n1)
{
long long *v = (long long *)calloc(1024, sizeof(long long));
*v = *n0 + *n1;
return v;
}

调用方循环执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *child(void *arg)
{
long long n0 = 0;
long long n1 = 1;
long long *v = NULL;

for (int n = 2; n > 0; n++) {
v = fibonacci(&n0, &n1);
n0 = n1;
n1 = *v;

printf("%dth => %lld\n", n, *v);
sleep(1);
}
}

问题很明显:

1
calloc() -> return -> 下一轮覆盖 v -> 之前的地址丢失

修复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void *child(void *arg)
{
long long n0 = 0;
long long n1 = 1;
long long *v = NULL;

for (int n = 2; n > 0; n++) {
v = fibonacci(&n0, &n1);
n0 = n1;
n1 = *v;

printf("%dth => %lld\n", n, *v);

free(v);
sleep(1);
}
}

更进一步,如果根本没必要动态分配,就可以直接改成栈变量或调用方提供缓冲区。

这比“记得 free”更进一步:

最好的泄漏修复之一,是减少不必要的动态内存所有权。


22. 为什么真实项目里的内存泄漏比这个案例难得多

真实程序常见复杂点包括:

  • malloc()free() 不在同一个函数;
  • 成功路径释放了,异常路径忘记释放;
  • 一个线程分配,另一个线程释放;
  • 第三方库隐式分配,调用方需要显式销毁;
  • 对象还被某个集合、缓存、回调、线程本地变量引用;
  • 每小时只泄漏几 MB,可能运行数周才暴露。

所以生产环境中最关键的是:

  1. 持续监控;
  2. 保留历史趋势;
  3. 能把内存增长与业务事件、日志对应起来。

内存泄漏不是一定要等 OOM 才发现。


23. 文件页与匿名页:理解内存回收和 Swap 的分水岭

Linux 内存回收时,一个特别重要的分类是:

File-backed Page(文件页)

典型包括:

  • Page Cache;
  • 文件映射;
  • 可重新从文件读取的页面。

如果文件页是干净的:

1
直接丢弃 -> 以后需要再从文件读取

如果文件页是脏的:

1
先回写磁盘 -> 再释放

Anonymous Page(匿名页)

典型包括:

  • 堆;
  • 栈;
  • 匿名 mmap()
  • 没有文件可以重新恢复内容的进程内存。

匿名页不能简单丢弃。

如果要回收,就需要把内容保存到其他地方,这就是 Swap 的核心作用。


24. LRU:Linux 怎么判断哪些页更值得回收

资料用经典的 Active/Inactive LRU 模型解释回收优先级。

可以把物理页大致理解成维护在两组链表中:

  • active
  • inactive

并进一步区分:

  • anonymous pages;
  • file pages。

查看示例:

1
cat /proc/meminfo | grep -i active | sort

可能看到:

1
2
3
4
5
6
Active:
Active(anon):
Active(file):
Inactive:
Inactive(anon):
Inactive(file):

核心思想不是“严格删除最久没用的一页”,而是:

通过活跃/非活跃状态近似判断页面近期使用情况,优先回收更冷的页面。

文件页冷了,可以丢弃或回写。

匿名页冷了,如果开启 Swap,可以换出。


25. 内存回收有两条路:Direct Reclaim 与 kswapd

Direct Reclaim(直接回收)

某个进程要申请内存,但当前可分配页不够时,内核可能让当前分配路径直接参与回收。

这会增加请求延迟,因为应用本来只是“申请内存”,却被迫同步做了回收工作。

kswapd

Linux 还有后台回收线程:

1
kswapd

它会根据内存水位提前回收,尽量避免所有压力都落到前台分配路径。

因此线上看到:

  • kswapd CPU 升高;
  • 应用延迟增加;
  • Swap/回收活动明显;

往往说明系统已经进入内存压力状态。


26. Watermark:Linux 怎么判断“内存压力大不大”

资料使用三个水位解释内存压力:

  • pages_min
  • pages_low
  • pages_high

可以从:

1
cat /proc/zoneinfo

中看到某个 Zone 的:

1
2
3
4
pages free
min
low
high

概念上可以理解为:

1
2
3
4
5
6
7
8
9
10
11
free > high
-> 内存宽裕

low < free <= high
-> 有压力,但通常还能满足请求

min < free <= low
-> kswapd 需要积极回收,直到恢复到较高水位

free <= min
-> 压力非常高,前台分配更容易进入直接回收甚至失败

资料给出的经典计算示例是:

1
2
pages_low  = pages_min * 5 / 4
pages_high = pages_min * 3 / 2

这些公式非常适合帮助理解水位之间的相对关系,但内核内部的精确计算逻辑会随版本演进,排障时应以当前内核的 /proc/zoneinfo 实际值为准。

pages_min 相关的系统配置入口是:

1
cat /proc/sys/vm/min_free_kbytes

27. Swap:不是“扩容内存”,而是用延迟换生存空间

Swap 可以是:

  • Swap 分区;
  • Swap 文件。

核心过程只有两个:

Swap Out

1
冷匿名页 -> 写入磁盘 -> 释放物理页

Swap In

1
进程再次访问 -> Page Fault -> 从磁盘读回 -> 恢复映射

Swap 的确让系统在物理内存不足时拥有更多缓冲空间,但代价非常明显:

磁盘访问延迟远高于内存。

一旦工作集频繁在内存和 Swap 之间来回抖动,就会出现严重性能下降。


28. swappiness 到底是什么

Linux 提供:

1
cat /proc/sys/vm/swappiness

来调整回收时对匿名页与文件页的倾向。

资料中的核心结论非常重要:

swappiness 是倾向,不是“用了多少百分比内存后才开始 Swap”。

也就是说:

1
swappiness = 0

并不应该简单理解成:

1
绝对禁止 Swap

如果内存压力足够大,系统仍可能被迫做匿名页回收。

如果业务明确不允许 Swap,真正可靠的方式是不要启用 Swap,而不是只改一个倾向参数。


29. NUMA:为什么整台机器还有空闲内存,却已经开始 Swap

NUMA(Non-Uniform Memory Access)机器上,CPU 和内存被划分成多个 Node。

每个 Node 更偏好使用本地内存:

1
2
3
Node 0 CPUs -> Node 0 Memory
Node 1 CPUs -> Node 1 Memory
...

本地访问通常更快,跨 Node 访问成本更高。

查看 NUMA:

1
numactl --hardware

示例结构:

1
2
3
4
5
6
7
8
available: 2 nodes (0-1)
node 0 cpus: ...
node 0 size: ...
node 0 free: ...

node 1 cpus: ...
node 1 size: ...
node 1 free: ...

所以不能只看整机:

1
MemAvailable

还应该考虑:

1
某个 Node 是否已经接近本地低水位

Linux 还提供:

1
cat /proc/sys/vm/zone_reclaim_mode

来影响 NUMA 本地回收策略。

资料总结的核心理解是:

  • 默认策略可以在本地回收,也可以寻找其他 Node;
  • 某些配置会更偏向只在本地回收;
  • 本地内存压力可能让 Swap/回收活动提前出现。

这解释了一个线上非常反直觉的现象:

“整机还有很多 free/available”并不能排除某个 NUMA Node 已经很紧张。


30. 一个完整 Swap 排查案例

资料通过大规模块设备读取让 Buffer 持续增长,然后观察 Swap 为什么开始上升。

30.1 开启 Swap 文件

示例:

1
2
3
4
fallocate -l 8G /mnt/swapfile
chmod 600 /mnt/swapfile
mkswap /mnt/swapfile
swapon /mnt/swapfile

确认:

1
free -h

30.2 构造大规模读取

1
dd if=/dev/sda1 of=/dev/null bs=1G count=2048

30.3 观察系统内存和 Swap

1
sar -r -S 1

关注:

  • kbmemfree
  • kbavail
  • kbmemused
  • %memused
  • kbbuffers
  • kbcached
  • kbactive
  • kbinact
  • kbswpfree
  • kbswpused
  • %swpused

现象大致是:

1
2
3
4
5
6
7
8
9
10
11
12
13
Buffer 持续增长
->
free 不断下降
->
触及回收水位
->
回收一部分文件页和匿名页
->
Swap 开始增长
->
dd 继续读,Buffer 再次增长
->
系统不断在“重新缓存 -> 回收”之间循环

30.4 用 cachetop 找出是谁把 Buffer 推高

1
cachetop 5

如果看到 dd 大量 Miss,就能把 Buffer 增长和这个进程关联起来。

30.5 观察 Watermark

1
watch -d "grep -A 15 'Normal' /proc/zoneinfo"

重点看:

1
2
3
4
5
6
7
8
pages free
min
low
high
nr_zone_inactive_anon
nr_zone_active_anon
nr_zone_inactive_file
nr_zone_active_file

如果 pages_free 下降到 low 附近后又突然回升,通常意味着回收正在发生。

30.6 找出哪些进程被换出了

每个进程的 Swap 可以从:

1
/proc/<pid>/status

中的:

1
VmSwap

获取。

资料给出一个排序脚本,可以整理成:

1
2
3
4
5
6
7
8
9
10
11
12
for file in /proc/[0-9]*/status; do
awk '
/^Name:/ {name=$2}
/^Pid:/ {pid=$2}
/^VmSwap:/ {swap=$2}
END {
if (swap > 0) {
printf "%-24s %-10s %10d kB\n", name, pid, swap
}
}
' "$file" 2>/dev/null
done | sort -k3 -nr | head

也可以借助支持 PSS/Swap 统计的工具,例如:

1
smem --sort swap

31. 清理 Swap:swapoff -a && swapon -a 有风险

资料给出了:

1
swapoff -a && swapon -a

作为一种清理 Swap 的方式。

这个命令的本质是:

1
把已经换出的匿名页强制重新换入物理内存

如果系统本来就是因为内存压力才用了 Swap,那么此时强制全部换回可能让物理内存瞬间更紧张,甚至触发 OOM。

因此线上不要把它当“无脑清 Swap”命令。

真正应该先回答:

  • 为什么产生了 Swap?
  • 哪些进程被换出?
  • 当前是否还有足够 MemAvailable
  • 是否正在持续 si/so
  • 是历史遗留的少量 Swap,还是正在发生 Thrashing?

32. OOM:最后一道保护机制

当回收文件页、回收匿名页等手段仍无法满足内存分配需求时,Linux 可能进入 OOM 路径,选择终止进程释放内存。

查看历史日志:

1
dmesg | grep -Ei 'out of memory|oom|killed process'

32.1 oom_score 与 oom_score_adj

Linux 会给进程计算 OOM 相关分数。

查看:

1
cat /proc/<pid>/oom_score

调整优先级时,现代接口更常用:

1
/proc/<pid>/oom_score_adj

资料早期还使用过:

1
/proc/<pid>/oom_adj

这是旧接口,理解历史材料时要区分。

不要把“把核心服务调成最难杀”当成万能方案。OOM 保护要和:

  • cgroups;
  • 容器内存 limit;
  • 服务优先级;
  • 系统保底进程;

一起设计。

否则所有进程都“不能杀”,最后就等于没有策略。


33. 一个必须纠正的 OOM 误区:不是“虚拟内存申请一大就立刻 OOM”

资料答疑中曾把 OOM 简化为“基于虚拟内存触发”。同一份资料后续评论已经指出,这个说法并不严谨。

例如:

1
void *p = malloc(500 * 1024 * 1024);

在允许 Overcommit 的系统中,malloc() 很可能成功,因为它首先只是获得一段虚拟地址空间。

如果之后不断写:

1
memset(p, 0, 500 * 1024 * 1024);

First Touch 才会逐页分配物理内存。

因此更准确的理解应该是:

OOM 的关键不是“VIRT 看起来多大”,而是某次实际内存分配/缺页在经过回收等机制后仍无法满足。

在容器环境中,还可能因为 cgroup 内存上限触发容器级 OOM,而整机仍然有可用内存。

这也是为什么排查 OOM 时,要同时看:

  • VIRT
  • RSS
  • PSS
  • MemAvailable
  • cgroup limit
  • dmesg
  • /proc/<pid>/status
  • 容器运行时事件

而不是只盯 VIRT


34. 从“内存高”到根因:一套可复用的排查流程

资料最后把前面所有知识收敛成一个非常重要的思想:

不要一上来把所有工具跑一遍。先用覆盖面大的工具判断问题类型,再进入专用工具。

可以整理成下面这套流程。

flowchart TD
    A[出现延迟/OOM/内存告警] --> B[free -h + top]
    B --> C{MemAvailable 是否低?}

    C -->|否| D{是否只是 Cache/Buffer 高?}
    D -->|是| E[vmstat/sar 看趋势]
    E --> F{Cache 是否持续增长?}
    F -->|是| G[cachetop/cachestat/slabtop/pcstat]
    F -->|否| H[可能是正常缓存]

    C -->|是| I[vmstat 1 / sar -r -S 1]
    I --> J{si/so 是否持续?}
    J -->|是| K[Swap/NUMA 分析]
    K --> L[/proc/zoneinfo + VmSwap + numactl]

    I --> M{某进程 RSS 是否持续增长?}
    M -->|是| N[pidstat/top 找进程]
    N --> O[pmap/smaps 看增长区域]
    O --> P[memleak/valgrind/语言运行时工具]

    I --> Q{Major Fault 是否高?}
    Q -->|是| R[检查 Swap / 文件 I/O / 工作集]

    B --> S{已经发生 OOM?}
    S -->|是| T[dmesg + cgroup + oom_score]

35. 第一层:free + top,先回答“系统到底缺不缺内存”

先看:

1
free -h

再看:

1
top

第一轮不要急着找代码,先回答:

  • available 是否真的低?
  • Swap 是否已经使用?
  • 哪些进程 RES/RSS 最大?
  • Cache/Buffer 是否占了大头?
  • 是否只是一个很大的 VIRT 吓到了你?

这一层的目标不是定根因,而是把问题分到几个大类:

1
2
3
4
5
6
A. 正常缓存
B. 进程常驻内存太高
C. 内存在持续增长
D. Swap 正在发生
E. OOM
F. Slab/内核内存异常

36. 第二层:vmstat / sar / pidstat,看变化趋势

瞬时值经常会骗人。

例如:

1
RSS = 8 GB

可能是:

  • 服务启动后就一直 8 GB,完全稳定;
  • 每分钟从 1 GB 增加到 8 GB;
  • GC 后会回落;
  • Page Cache 的变化造成系统 used 上升;
  • 只是短时间峰值。

所以需要持续观察。

vmstat

1
vmstat 1

常看:

  • free
  • buff
  • cache
  • si
  • so
  • bi
  • bo
  • wa

sar

1
sar -r -S 1

适合同时看:

  • 内存占用;
  • 活跃/非活跃内存;
  • Swap。

pidstat

1
pidstat -r 1

适合按进程观察:

  • RSS;
  • 缺页;
  • 内存增长趋势。

37. 第三层:针对问题类型选择工具

问题 优先工具
系统整体内存 free/proc/meminfo
动态趋势 vmstatsar
进程 RSS/VIRT toppspidstat
进程地址空间 pmap/proc/<pid>/smaps
进程 Swap /proc/<pid>/statussmem
Page Cache 命中 cachestatcachetop
指定文件缓存 pcstat
Slab slabtop/proc/slabinfo
内存泄漏 memleakvalgrind
系统调用 strace
NUMA numactl --hardware/proc/zoneinfo
OOM dmesgoom_score、cgroup 指标

核心原则是:

从现象选指标,再从指标选工具。不要反过来“因为我会这个工具,所以我只看这个工具”。


38. 常见症状与快速判断

症状一:free 很小

不要立刻扩容。

先看:

1
2
3
available
buff/cache
si/so

如果:

1
2
3
free 很小
available 很大
swap 不活跃

大概率只是 Linux 正常使用缓存。


症状二:buff/cache 很大

继续看趋势:

1
vmstat 1

如果 Cache 持续增大:

  • 大文件扫描?
  • 文件热数据?
  • 大量 mmap?
  • Slab 增长?

再分别用:

1
2
3
4
cachetop
cachestat
slabtop
pcstat

缩小范围。


症状三:进程 VIRT 特别大

先不要慌。

看:

1
2
3
RES/RSS
PSS
VmSwap

如果只是 VIRT 大而 RSS 稳定,可能只是:

  • 预留地址空间;
  • mmap;
  • JVM 等运行时的地址空间管理;
  • 尚未 First Touch 的内存。

症状四:进程 RSS 一直增长

这是最值得怀疑泄漏的信号之一。

路径:

1
2
3
4
5
6
7
pidstat/top
->
pmap/smaps
->
确定是 heap/anon/file map
->
memleak/valgrind/runtime profiler

症状五:Swap 已经用了很多

先判断:

1
现在还在 swap,还是历史上曾 swap?

看:

1
vmstat 1

如果 si/so 长时间接近 0:

  • 可能只是历史遗留的冷页;
  • Swap 已用不一定意味着当前正在产生明显性能问题。

如果 si/so 持续很高:

  • 工作集大于物理内存;
  • 系统可能正在 Thrashing;
  • 延迟通常会显著恶化。

症状六:OOM

先找日志:

1
dmesg | grep -Ei 'out of memory|oom|killed process'

然后区分:

  • Host OOM;
  • cgroup/container OOM;
  • 单进程分配失败;
  • overcommit/commit limit;
  • 内核内存耗尽。

再回头找“为什么内存到这里了”,而不是只把被杀进程重启。


39. 性能测试最容易踩的缓存坑

坑一:同一个文件重复测,第二次其实在测内存

如果你要测冷缓存文件读性能:

1
echo 3 > /proc/sys/vm/drop_caches

只适合测试环境。

如果要测实际线上应用体验,那么 Page Cache 本来就是系统性能的一部分,不应该人为清掉。

坑二:用 O_DIRECT 后拿结果和普通 Buffered I/O 直接比较

两者优化目标不同。

Buffered I/O:

1
利用 Page Cache

Direct I/O:

1
绕过 Page Cache

数据库、自带 Buffer Pool 的存储系统常常会考虑 Direct I/O,是为了避免双重缓存,而不是因为 Direct I/O 天生更快。

坑三:把设备吞吐和文件系统吞吐混为一谈

1
dd if=/dev/sda1 ...

与:

1
dd if=/mnt/data/file ...

走的层次不同。

比较之前必须明确测试对象。


40. proc 文件系统为什么是性能分析必修课

很多性能工具本质上只是把 /proc 中的信息格式化给你看。

常见接口:

1
2
3
4
5
6
7
8
9
/proc/meminfo
/proc/zoneinfo
/proc/<pid>/status
/proc/<pid>/smaps
/proc/<pid>/oom_score
/proc/<pid>/oom_score_adj
/proc/sys/vm/swappiness
/proc/sys/vm/min_free_kbytes
/proc/sys/vm/zone_reclaim_mode

遇到不认识的指标,正确习惯不是先背博客结论,而是:

1
2
3
4
5
man free
man proc
man vmstat
man sar
man top

性能指标会随:

  • 内核版本;
  • procps/sysstat 版本;
  • 发行版;

发生变化。

理解数据来源,比记住某个版本的输出列更可靠。


41. 几个资料中需要特别注意的历史/版本问题

这组资料来自 2018-2019 年,原理仍然非常有价值,但部分命令、接口和表述不能直接当成今天所有 Linux 环境的固定事实。

41.1 “64 位用户空间 128 TB”是经典示意,不是永恒常量

虚拟地址宽度和页表级数会随架构变化。

要记模型,不要死记容量。

41.2 “Linux 使用四级页表”是当时主流 x86_64 语境

现代系统可能支持五级页表。

41.3 “malloc 小于 128 KB 用 brk,大于 128 KB 用 mmap”是经典 glibc 解释

真实分配器会动态调整阈值,也可能使用其他 allocator。

41.4 oom_adj 是旧接口

资料早期命令:

1
echo -16 > /proc/$(pidof sshd)/oom_adj

适合理解历史。

现代排障更常见的是:

1
2
oom_score
oom_score_adj

41.5 “RSS 不包含共享内存”是过度简化

资料后续答疑已经指出该定义和工具实际输出存在出入。

多进程汇总使用 PSS 更可靠。

41.6 “页表存储在 MMU”是简化错误

更准确的是:

1
2
3
页表在内存
MMU 负责地址翻译
TLB 缓存翻译结果

41.7 “OOM 基于虚拟内存触发”不能机械理解

大虚拟地址空间本身不等于 OOM。

真正要看物理页兑现、内存回收、cgroup 限制和分配失败。

41.8 BCC 安装命令具有很强时代性

资料中的 Ubuntu/CentOS 仓库命令是当时实践环境。

今天不要为了照抄一篇旧文章,直接在生产服务器升级内核。

原则应该是:

1
2
先确认现象和需要的指标
-> 再选择当前环境能使用的工具

工具受限时依然可以用:

  • /proc
  • vmstat
  • sar
  • pmap
  • smaps
  • strace
  • valgrind
  • 语言运行时自带工具

解决大量问题。


42. 内存优化真正值得优先做什么

42.1 让热点数据留在内存

这是一切缓存优化的根本。

热点数据反复从磁盘读取,性能一定差。

可以利用:

  • Page Cache;
  • 应用内存缓存;
  • Redis 等外部缓存;
  • 数据库 Buffer Pool。

但缓存一定要有边界和淘汰策略。


42.2 减少无意义的动态内存分配

高频:

1
malloc -> free -> malloc -> free

可能带来:

  • allocator 锁竞争;
  • 碎片;
  • Page Fault;
  • mmap/brk 管理成本;
  • Cache Locality 变差。

常见优化:

  • 对象池;
  • 内存池;
  • 批量分配;
  • 复用缓冲区;
  • 合理使用栈;
  • 大页。

42.3 尽量避免 Swap 进入请求关键路径

延迟敏感服务通常应该把工作集控制在物理内存范围内。

比“调 swappiness”更重要的是:

  • 容量规划;
  • cgroups limit;
  • 工作集大小;
  • 缓存上限;
  • 防止泄漏。

Swap 可以做保险,但不要让业务正常路径依赖频繁 Swap。


42.4 使用 cgroups 限制异常进程

单个进程或容器如果可以无限吃内存,很容易拖垮整机。

合理的内存限制至少要结合:

  • 正常稳态;
  • 峰值;
  • GC/Compaction;
  • Page Cache;
  • 本地缓存;
  • 临时大对象;
  • 业务突发。

不要只测“刚启动时 RSS”就决定容器内存上限。


42.5 给核心进程合理设置 OOM 优先级

如果机器真的走到 OOM,至少要确保:

  • SSH/运维入口;
  • 核心控制进程;
  • 系统守护进程;

不会最先被杀。

但 OOM 优先级只是最后防线,不能代替容量治理。


43. 一份可以直接用于线上排障的命令清单

系统总览

1
2
3
free -h
top
vmstat 1

进程排序

1
ps aux --sort=-rss | head -20

进程内存

1
2
3
pmap -x <pid>
cat /proc/<pid>/status
cat /proc/<pid>/smaps

Page Cache / Buffer

1
2
3
4
cat /proc/meminfo
cachestat 1
cachetop
slabtop

文件缓存

1
pcstat <file>

Page Fault / 进程趋势

1
pidstat -r 1

Swap

1
2
3
4
5
free -h
vmstat 1
sar -r -S 1
cat /proc/sys/vm/swappiness
cat /proc/zoneinfo

NUMA

1
2
numactl --hardware
cat /proc/sys/vm/zone_reclaim_mode

OOM

1
2
3
dmesg | grep -Ei 'out of memory|oom|killed process'
cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj

系统调用

1
strace -p <pid>

内存泄漏

1
memleak -p <pid>

或:

1
valgrind --leak-check=full ./app

44. 一套更适合软件开发者记忆的内存性能模型

可以把整个内存篇压缩成四层。

第一层:地址

1
2
3
4
5
Virtual Address
->
Page Table / TLB
->
Physical Page

这一层决定:

  • 地址空间;
  • 页表;
  • TLB;
  • Page Fault;
  • HugePage。

第二层:分配

1
2
3
4
5
6
7
8
9
malloc
->
brk / mmap
->
First Touch
->
Buddy
->
Physical Page

这一层决定:

  • VIRT 与 RSS 为什么不一样;
  • 内存碎片;
  • 动态分配开销;
  • 泄漏。

第三层:缓存与回收

1
2
3
4
5
6
7
File Page
-> Page Cache
-> Reclaim / Writeback

Anonymous Page
-> RAM
-> Swap

这一层决定:

  • Buffer/Cache;
  • LRU;
  • Dirty Page;
  • Swap;
  • kswapd;
  • Watermark。

第四层:故障处理

1
2
3
4
5
6
7
8
内存持续增长
-> 泄漏分析

回收频繁
-> Cache/Swap/NUMA 分析

回收仍不足
-> OOM

这一层决定线上排查方法。


45. 最后:不要把“工具”当知识体系,把“关系”记下来

Linux 内存相关工具很多,但它们其实只是在回答不同层次的问题。

free 回答:

1
整台机器现在还有多少内存余量?

vmstat/sar 回答:

1
内存是在变好还是变坏?Swap 和回收是不是正在发生?

top/pidstat 回答:

1
哪个进程最可疑?

pmap/smaps 回答:

1
这个进程的内存到底长在哪个地址区间?

cachestat/cachetop 回答:

1
这些缓存到底有没有被有效利用?

memleak/valgrind 回答:

1
哪次内存分配没有被释放?

zoneinfo/numactl 回答:

1
是不是已经触发内存水位或 NUMA 本地压力?

dmesg 回答:

1
内核最后为什么决定 OOM?

真正稳定的分析方法可以归纳成一句话:

先看系统是否真的有内存压力,再看压力来自 Cache、进程常驻集、内存泄漏还是 Swap;确认类型后,沿着指标找到进程,再沿着进程找到地址空间、系统调用和代码。

只要这条主线还在,即使换了 Linux 版本、换了工具、换了语言运行时,也不会因为某个命令不存在就失去分析能力。


Linux 内存管理与性能优化:从虚拟内存、Page Cache 到 Swap、OOM 与泄漏排查
https://allendericdalexander.github.io/2026/08/11/devops/linux/performanceOptimization/03linux-memory-management-performance-optimization/
作者
AtLuoFu
发布于
2026年8月11日
许可协议