Linux 内存管理与性能优化:从虚拟内存、Page Cache 到 Swap、OOM 与泄漏排查
Linux 内存管理与性能优化:从虚拟内存、Page Cache 到 Swap、OOM 与泄漏排查
Linux 内存性能问题很容易陷入两个极端:一边是只会看 free、top,看到 free 很小就认定“内存不够”;另一边是一下子钻进页表、LRU、NUMA、Swap、Slab、OOM 的细节,知道很多名词,却不知道线上出问题时该先看什么。
真正有用的知识结构应该把几条链路串起来:
- 进程看到的是虚拟地址,真正使用的是物理页。
- 内存分配并不等于物理内存立刻到手,首次访问才可能触发缺页并分配物理页。
- Linux 会把空闲内存积极用于缓存,因此
free很低并不等价于内存紧张。 - 文件页、匿名页的回收方式不同,进而引出 Page Cache、Swap、LRU、OOM。
- 性能分析不能只看一个瞬时值,而要从系统整体、变化趋势、具体进程一路缩小范围。
把这些关系建立起来以后,free、vmstat、sar、pmap、smaps、cachetop、memleak 等工具就不再是一串需要死记硬背的命令,而是对应内存工作机制的不同观察窗口。
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 内存性能分析,本质上是在回答四个问题:
- 内存被谁占了?
- 这些内存是什么类型?
- 它们是否可以被回收?
- 系统是否因为回收、缺页、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 | |
如果为每个虚拟页都保存一条映射,页表会非常庞大。因此 Linux 使用 Multi-level Page Table(多级页表),只为实际使用到的地址空间建立必要的页表层级。
资料以经典四级页表说明虚拟地址可以拆成:
1 | |
现代体系结构上页表级数可能更多,例如某些 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 敏感的场景。
大页的主要收益包括:
- 减少页表项数量;
- 提高单个 TLB Entry 覆盖的内存范围;
- 降低地址翻译开销。
但大页并不是“打开就一定更快”。它会带来更粗的内存分配粒度,可能增加内存浪费,也会改变内存回收和碎片管理方式。因此应当基于负载特征使用,而不是把 HugePage 当成通用开关。
5. 一个 Linux 进程的虚拟地址空间怎么分布
以经典布局理解,一个用户进程通常会包含:
| 区域 | 主要内容 | 典型特征 |
|---|---|---|
| 只读段 | 程序代码、常量 | 通常只读,可被多个进程共享 |
| 数据段 | 全局变量、静态变量 | 生命周期通常与进程一致 |
| Heap(堆) | 动态分配内存 | 由应用/分配器管理 |
| Memory Mapping Area(映射区) | 动态库、文件映射、共享内存、匿名映射 | 常由 mmap() 建立 |
| Stack(栈) | 局部变量、函数调用上下文 | 自动分配和回收 |
资料用“堆从低地址向高地址增长、文件映射区从高地址向低地址增长”的经典图帮助理解。
需要注意,现代系统启用了 ASLR(地址空间布局随机化)后,具体地址位置并不会像教材图那样固定。栈大小也不是永远固定为 8 MB,而是受 ulimit、线程实现和系统配置影响。
查看当前 Shell 的栈限制可以使用:
1 | |
6. malloc、brk、mmap:申请虚拟内存不等于立刻占用物理内存
C 程序中最常见的动态内存入口是:
1 | |
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 | |
并不意味着内核立即拿出 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/slabinfo、slabtop 这些接口。
这里必须区分一个常见误解:
用户空间的
malloc()不会直接使用内核 Slab 来管理用户对象。Slab 主要用于内核空间小对象。
用户空间分配器向内核申请页或映射,再在用户空间内部做更细粒度的切分和复用。
8. 怎么正确读 free:free 小不等于内存紧张
最常见的系统内存入口是:
1 | |
典型指标包括:
| 指标 | 含义 |
|---|---|
total |
总物理内存 |
used |
已使用内存 |
free |
当前完全未使用的内存 |
shared |
共享内存相关使用量 |
buff/cache |
Buffer、Page Cache、可回收 Slab 等缓存 |
available |
内核估计的新进程在不发生明显 Swap 的情况下可用的内存 |
真正需要重点观察的不是单独的 free,而是:
1 | |
Linux 的设计倾向是:
与其让物理内存闲着,不如拿来做缓存。
所以一个运行稳定的 Linux 服务器,free 很小、buff/cache 很大,完全可能是健康状态。
判断内存压力时应该结合:
available;- Swap 是否活跃;
vmstat的si/so;- Major Fault 是否升高;
- 回收线程是否频繁活动;
- 应用延迟是否与内存回收同步恶化。
9. 进程内存指标:VIRT、RES/RSS、SHR、PSS 到底怎么看
9.1 VIRT / VSS
VIRT 或 VSS 表示进程拥有的虚拟地址空间,可能包括:
- 代码;
- 数据;
- 堆;
- 映射文件;
- 动态库;
- 共享内存;
- 已申请但尚未真正映射物理页的区域;
- 已经换出到 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 相加,可以避免共享页被重复统计。
例如:
1 | |
如果要深入看单个进程:
1 | |
或者:
1 | |
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:两个最容易被误解的指标
free 的 buff/cache 实际包含多个来源。
资料从 /proc/meminfo 和 man proc 出发,梳理了三个关键值:
BuffersCachedSReclaimable
可以把它们理解为:
| 指标 | 核心含义 |
|---|---|
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 | |
/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 | |
这里直接读块设备文件,不通过某个普通文件的文件系统语义。
概念上可以理解为:
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 | |
后续重复读:
1 | |
这就是为什么同一个文件第二次读往往快很多。
14. drop_caches 适合实验,不适合当“优化命令”
资料为了减少实验干扰使用:
1 | |
它会释放可以回收的 Page Cache、dentry、inode 等缓存。
这个命令非常适合:
- 做缓存冷启动实验;
- 对比第一次读取与第二次读取;
- 验证某个现象是否与系统缓存有关。
但不应该把它当成常规“释放内存优化”。
原因很简单:
你主动清掉的,正是 Linux 为了加速后续 I/O 而保留的缓存。
如果每次看到 Cache 高就 drop_caches,相当于把操作系统主动做好的性能优化扔掉,然后让下一个请求重新读盘。
15. 系统缓存到底有没有用:用命中率说话
缓存是否有效,不能只看“缓存占了多少内存”,更应该看 Cache Hit Ratio(缓存命中率)。
基本公式:
1 | |
命中率越高,说明更多请求可以在内存中完成,减少慢速磁盘 I/O。
资料使用 BCC 中的:
cachestatcachetop
来观察缓存。
15.1 cachestat
用于看系统整体缓存读写情况,典型字段包括:
| 字段 | 含义 |
|---|---|
TOTAL |
总 I/O 次数 |
MISSES |
缓存未命中 |
HITS |
缓存命中 |
DIRTIES |
新产生的脏页 |
BUFFERS_MB |
Buffer 大小 |
CACHED_MB |
Cache 大小 |
示例:
1 | |
15.2 cachetop
用于按进程观察缓存命中。
1 | |
常见字段:
HITSMISSESDIRTIESREAD_HIT%WRITE_HIT%
15.3 pcstat
pcstat 可以查看某个具体文件有多少页面已经在 Page Cache 中。
例如:
1 | |
这比“整个系统 Cache 有多大”更接近某个文件的实际缓存状态。
资料里的 BCC 和 pcstat 安装步骤来自 2018-2019 年环境,仓库地址、包名和安装方式具有明显时代性。真正使用时应优先查当前发行版和项目文档,而不是照抄旧安装命令。
16. 一个非常典型的缓存实验:为什么第二次 dd 能从 33 MB/s 变成 4.5 GB/s
资料先构造一个 512 MB 文件,然后清理缓存:
1 | |
确认文件不在缓存:
1 | |
第一次读取:
1 | |
示例结果约:
1 | |
第二次读取同一文件:
1 | |
示例结果约:
1 | |
这当然不是磁盘突然变成了 4.5 GB/s。
第二次读已经基本命中 Page Cache,测到的是:
- 内存访问;
- 内核路径;
- 数据拷贝;
而不再是存储设备的真实吞吐。
这个案例直接给出一条性能测试原则:
测磁盘性能之前,必须先明确你要测“存储设备本身”,还是“文件系统 + Page Cache 的实际应用性能”。
否则同一个 dd 命令可以测出完全不同的东西。
17. O_DIRECT:缓存命中率很高,为什么应用还是读得很慢
资料中另一个案例每次读取约 32 MB,但耗时约 0.9 秒。
cachetop 看上去命中率是 100%,似乎应该很快,可根据页面数换算后发现缓存吞吐只有很小一部分,和应用实际读取量不一致。
进一步通过 strace:
1 | |
观察到:
1 | |
关键就是:
1 | |
源代码类似:
1 | |
删除 O_DIRECT 后,读取 32 MB 的时间从接近 0.9 秒下降到约 0.03 秒量级。
这个案例揭示了两个非常重要的排查经验。
第一,性能工具的“100%”并不等于你以为的 100%
cachetop 并不会把所有 Direct I/O 数据完整纳入同一种 Page Cache 统计逻辑。
所以看到:
1 | |
不能立刻得出“应用全部从缓存读取”。
还必须对照:
- 应用实际读了多少字节;
- 命中了多少页;
- 每页多大;
- 统计窗口多长。
如果指标之间算不平,就说明还有另一条 I/O 路径。
第二,性能分析最终要能落到系统调用
当现象和指标矛盾时:
1 | |
这比只说“磁盘慢”更接近真正的根因。
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 | |
函数结束后,栈帧被回收,不需要手动 free()。
堆内存
通过:
1 | |
等方式获得的动态内存,需要匹配:
1 | |
否则最容易形成经典堆内存泄漏。
文件映射和共享内存
mmap()、共享内存等映射资源同样需要正确解除。
典型接口:
1 | |
所以从进程地址空间看,最值得重点盯住的是:
- Heap;
- Anonymous Mapping;
- Shared Memory;
- 长期增长的映射区。
20. 怎么定位内存泄漏:先看趋势,再找调用栈
资料给出了一条非常实用的路径。
第一步:确认系统内存在持续下降
1 | |
如果持续看到:
free下降;buff/cache相对稳定;- Swap 尚未明显介入;
说明某类不可回收或暂时不能回收的内存正在增加。
但这仍然不能直接证明泄漏,因为业务可能本来就在建立一个合法的大缓存。
所以重点是“趋势 + 业务预期”。
第二步:定位增长最快的进程
可以用:
1 | |
按 M 按内存排序。
也可以:
1 | |
或者:
1 | |
第三步:查看进程地址空间
1 | |
以及:
1 | |
观察到底是:
- heap;
- anonymous mapping;
- file mapping;
- shared memory;
哪一部分在增长。
第四步:追踪“分配了但没有释放”的调用栈
资料使用 BCC 的 memleak:
1 | |
它可以跟踪:
- 内存分配;
- 内存释放;
- Outstanding Allocation;
- 对应调用栈。
如果生产环境内核太旧,资料还给出了一个重要替代思路:
1 | |
也就是说:
工具不是核心,核心是“找到仍未释放的分配记录,并映射回代码调用栈”。
21. 一个最小内存泄漏案例
资料中的 Fibonacci 示例,为了让泄漏更明显,每次分配 1024 个 long long:
1 | |
调用方循环执行:
1 | |
问题很明显:
1 | |
修复:
1 | |
更进一步,如果根本没必要动态分配,就可以直接改成栈变量或调用方提供缓冲区。
这比“记得 free”更进一步:
最好的泄漏修复之一,是减少不必要的动态内存所有权。
22. 为什么真实项目里的内存泄漏比这个案例难得多
真实程序常见复杂点包括:
malloc()和free()不在同一个函数;- 成功路径释放了,异常路径忘记释放;
- 一个线程分配,另一个线程释放;
- 第三方库隐式分配,调用方需要显式销毁;
- 对象还被某个集合、缓存、回调、线程本地变量引用;
- 每小时只泄漏几 MB,可能运行数周才暴露。
所以生产环境中最关键的是:
- 持续监控;
- 保留历史趋势;
- 能把内存增长与业务事件、日志对应起来。
内存泄漏不是一定要等 OOM 才发现。
23. 文件页与匿名页:理解内存回收和 Swap 的分水岭
Linux 内存回收时,一个特别重要的分类是:
File-backed Page(文件页)
典型包括:
- Page Cache;
- 文件映射;
- 可重新从文件读取的页面。
如果文件页是干净的:
1 | |
如果文件页是脏的:
1 | |
Anonymous Page(匿名页)
典型包括:
- 堆;
- 栈;
- 匿名
mmap(); - 没有文件可以重新恢复内容的进程内存。
匿名页不能简单丢弃。
如果要回收,就需要把内容保存到其他地方,这就是 Swap 的核心作用。
24. LRU:Linux 怎么判断哪些页更值得回收
资料用经典的 Active/Inactive LRU 模型解释回收优先级。
可以把物理页大致理解成维护在两组链表中:
activeinactive
并进一步区分:
- anonymous pages;
- file pages。
查看示例:
1 | |
可能看到:
1 | |
核心思想不是“严格删除最久没用的一页”,而是:
通过活跃/非活跃状态近似判断页面近期使用情况,优先回收更冷的页面。
文件页冷了,可以丢弃或回写。
匿名页冷了,如果开启 Swap,可以换出。
25. 内存回收有两条路:Direct Reclaim 与 kswapd
Direct Reclaim(直接回收)
某个进程要申请内存,但当前可分配页不够时,内核可能让当前分配路径直接参与回收。
这会增加请求延迟,因为应用本来只是“申请内存”,却被迫同步做了回收工作。
kswapd
Linux 还有后台回收线程:
1 | |
它会根据内存水位提前回收,尽量避免所有压力都落到前台分配路径。
因此线上看到:
- kswapd CPU 升高;
- 应用延迟增加;
- Swap/回收活动明显;
往往说明系统已经进入内存压力状态。
26. Watermark:Linux 怎么判断“内存压力大不大”
资料使用三个水位解释内存压力:
pages_minpages_lowpages_high
可以从:
1 | |
中看到某个 Zone 的:
1 | |
概念上可以理解为:
1 | |
资料给出的经典计算示例是:
1 | |
这些公式非常适合帮助理解水位之间的相对关系,但内核内部的精确计算逻辑会随版本演进,排障时应以当前内核的 /proc/zoneinfo 实际值为准。
pages_min 相关的系统配置入口是:
1 | |
27. Swap:不是“扩容内存”,而是用延迟换生存空间
Swap 可以是:
- Swap 分区;
- Swap 文件。
核心过程只有两个:
Swap Out
1 | |
Swap In
1 | |
Swap 的确让系统在物理内存不足时拥有更多缓冲空间,但代价非常明显:
磁盘访问延迟远高于内存。
一旦工作集频繁在内存和 Swap 之间来回抖动,就会出现严重性能下降。
28. swappiness 到底是什么
Linux 提供:
1 | |
来调整回收时对匿名页与文件页的倾向。
资料中的核心结论非常重要:
swappiness 是倾向,不是“用了多少百分比内存后才开始 Swap”。
也就是说:
1 | |
并不应该简单理解成:
1 | |
如果内存压力足够大,系统仍可能被迫做匿名页回收。
如果业务明确不允许 Swap,真正可靠的方式是不要启用 Swap,而不是只改一个倾向参数。
29. NUMA:为什么整台机器还有空闲内存,却已经开始 Swap
NUMA(Non-Uniform Memory Access)机器上,CPU 和内存被划分成多个 Node。
每个 Node 更偏好使用本地内存:
1 | |
本地访问通常更快,跨 Node 访问成本更高。
查看 NUMA:
1 | |
示例结构:
1 | |
所以不能只看整机:
1 | |
还应该考虑:
1 | |
Linux 还提供:
1 | |
来影响 NUMA 本地回收策略。
资料总结的核心理解是:
- 默认策略可以在本地回收,也可以寻找其他 Node;
- 某些配置会更偏向只在本地回收;
- 本地内存压力可能让 Swap/回收活动提前出现。
这解释了一个线上非常反直觉的现象:
“整机还有很多 free/available”并不能排除某个 NUMA Node 已经很紧张。
30. 一个完整 Swap 排查案例
资料通过大规模块设备读取让 Buffer 持续增长,然后观察 Swap 为什么开始上升。
30.1 开启 Swap 文件
示例:
1 | |
确认:
1 | |
30.2 构造大规模读取
1 | |
30.3 观察系统内存和 Swap
1 | |
关注:
kbmemfreekbavailkbmemused%memusedkbbufferskbcachedkbactivekbinactkbswpfreekbswpused%swpused
现象大致是:
1 | |
30.4 用 cachetop 找出是谁把 Buffer 推高
1 | |
如果看到 dd 大量 Miss,就能把 Buffer 增长和这个进程关联起来。
30.5 观察 Watermark
1 | |
重点看:
1 | |
如果 pages_free 下降到 low 附近后又突然回升,通常意味着回收正在发生。
30.6 找出哪些进程被换出了
每个进程的 Swap 可以从:
1 | |
中的:
1 | |
获取。
资料给出一个排序脚本,可以整理成:
1 | |
也可以借助支持 PSS/Swap 统计的工具,例如:
1 | |
31. 清理 Swap:swapoff -a && swapon -a 有风险
资料给出了:
1 | |
作为一种清理 Swap 的方式。
这个命令的本质是:
1 | |
如果系统本来就是因为内存压力才用了 Swap,那么此时强制全部换回可能让物理内存瞬间更紧张,甚至触发 OOM。
因此线上不要把它当“无脑清 Swap”命令。
真正应该先回答:
- 为什么产生了 Swap?
- 哪些进程被换出?
- 当前是否还有足够
MemAvailable? - 是否正在持续
si/so? - 是历史遗留的少量 Swap,还是正在发生 Thrashing?
32. OOM:最后一道保护机制
当回收文件页、回收匿名页等手段仍无法满足内存分配需求时,Linux 可能进入 OOM 路径,选择终止进程释放内存。
查看历史日志:
1 | |
32.1 oom_score 与 oom_score_adj
Linux 会给进程计算 OOM 相关分数。
查看:
1 | |
调整优先级时,现代接口更常用:
1 | |
资料早期还使用过:
1 | |
这是旧接口,理解历史材料时要区分。
不要把“把核心服务调成最难杀”当成万能方案。OOM 保护要和:
- cgroups;
- 容器内存 limit;
- 服务优先级;
- 系统保底进程;
一起设计。
否则所有进程都“不能杀”,最后就等于没有策略。
33. 一个必须纠正的 OOM 误区:不是“虚拟内存申请一大就立刻 OOM”
资料答疑中曾把 OOM 简化为“基于虚拟内存触发”。同一份资料后续评论已经指出,这个说法并不严谨。
例如:
1 | |
在允许 Overcommit 的系统中,malloc() 很可能成功,因为它首先只是获得一段虚拟地址空间。
如果之后不断写:
1 | |
First Touch 才会逐页分配物理内存。
因此更准确的理解应该是:
OOM 的关键不是“VIRT 看起来多大”,而是某次实际内存分配/缺页在经过回收等机制后仍无法满足。
在容器环境中,还可能因为 cgroup 内存上限触发容器级 OOM,而整机仍然有可用内存。
这也是为什么排查 OOM 时,要同时看:
VIRTRSSPSSMemAvailable- 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 | |
再看:
1 | |
第一轮不要急着找代码,先回答:
available是否真的低?- Swap 是否已经使用?
- 哪些进程 RES/RSS 最大?
- Cache/Buffer 是否占了大头?
- 是否只是一个很大的
VIRT吓到了你?
这一层的目标不是定根因,而是把问题分到几个大类:
1 | |
36. 第二层:vmstat / sar / pidstat,看变化趋势
瞬时值经常会骗人。
例如:
1 | |
可能是:
- 服务启动后就一直 8 GB,完全稳定;
- 每分钟从 1 GB 增加到 8 GB;
- GC 后会回落;
- Page Cache 的变化造成系统 used 上升;
- 只是短时间峰值。
所以需要持续观察。
vmstat
1 | |
常看:
freebuffcachesisobibowa
sar
1 | |
适合同时看:
- 内存占用;
- 活跃/非活跃内存;
- Swap。
pidstat
1 | |
适合按进程观察:
- RSS;
- 缺页;
- 内存增长趋势。
37. 第三层:针对问题类型选择工具
| 问题 | 优先工具 |
|---|---|
| 系统整体内存 | free、/proc/meminfo |
| 动态趋势 | vmstat、sar |
| 进程 RSS/VIRT | top、ps、pidstat |
| 进程地址空间 | pmap、/proc/<pid>/smaps |
| 进程 Swap | /proc/<pid>/status、smem |
| Page Cache 命中 | cachestat、cachetop |
| 指定文件缓存 | pcstat |
| Slab | slabtop、/proc/slabinfo |
| 内存泄漏 | memleak、valgrind |
| 系统调用 | strace |
| NUMA | numactl --hardware、/proc/zoneinfo |
| OOM | dmesg、oom_score、cgroup 指标 |
核心原则是:
从现象选指标,再从指标选工具。不要反过来“因为我会这个工具,所以我只看这个工具”。
38. 常见症状与快速判断
症状一:free 很小
不要立刻扩容。
先看:
1 | |
如果:
1 | |
大概率只是 Linux 正常使用缓存。
症状二:buff/cache 很大
继续看趋势:
1 | |
如果 Cache 持续增大:
- 大文件扫描?
- 文件热数据?
- 大量 mmap?
- Slab 增长?
再分别用:
1 | |
缩小范围。
症状三:进程 VIRT 特别大
先不要慌。
看:
1 | |
如果只是 VIRT 大而 RSS 稳定,可能只是:
- 预留地址空间;
- mmap;
- JVM 等运行时的地址空间管理;
- 尚未 First Touch 的内存。
症状四:进程 RSS 一直增长
这是最值得怀疑泄漏的信号之一。
路径:
1 | |
症状五:Swap 已经用了很多
先判断:
1 | |
看:
1 | |
如果 si/so 长时间接近 0:
- 可能只是历史遗留的冷页;
- Swap 已用不一定意味着当前正在产生明显性能问题。
如果 si/so 持续很高:
- 工作集大于物理内存;
- 系统可能正在 Thrashing;
- 延迟通常会显著恶化。
症状六:OOM
先找日志:
1 | |
然后区分:
- Host OOM;
- cgroup/container OOM;
- 单进程分配失败;
- overcommit/commit limit;
- 内核内存耗尽。
再回头找“为什么内存到这里了”,而不是只把被杀进程重启。
39. 性能测试最容易踩的缓存坑
坑一:同一个文件重复测,第二次其实在测内存
如果你要测冷缓存文件读性能:
1 | |
只适合测试环境。
如果要测实际线上应用体验,那么 Page Cache 本来就是系统性能的一部分,不应该人为清掉。
坑二:用 O_DIRECT 后拿结果和普通 Buffered I/O 直接比较
两者优化目标不同。
Buffered I/O:
1 | |
Direct I/O:
1 | |
数据库、自带 Buffer Pool 的存储系统常常会考虑 Direct I/O,是为了避免双重缓存,而不是因为 Direct I/O 天生更快。
坑三:把设备吞吐和文件系统吞吐混为一谈
1 | |
与:
1 | |
走的层次不同。
比较之前必须明确测试对象。
40. proc 文件系统为什么是性能分析必修课
很多性能工具本质上只是把 /proc 中的信息格式化给你看。
常见接口:
1 | |
遇到不认识的指标,正确习惯不是先背博客结论,而是:
1 | |
性能指标会随:
- 内核版本;
- 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 | |
适合理解历史。
现代排障更常见的是:
1 | |
41.5 “RSS 不包含共享内存”是过度简化
资料后续答疑已经指出该定义和工具实际输出存在出入。
多进程汇总使用 PSS 更可靠。
41.6 “页表存储在 MMU”是简化错误
更准确的是:
1 | |
41.7 “OOM 基于虚拟内存触发”不能机械理解
大虚拟地址空间本身不等于 OOM。
真正要看物理页兑现、内存回收、cgroup 限制和分配失败。
41.8 BCC 安装命令具有很强时代性
资料中的 Ubuntu/CentOS 仓库命令是当时实践环境。
今天不要为了照抄一篇旧文章,直接在生产服务器升级内核。
原则应该是:
1 | |
工具受限时依然可以用:
/procvmstatsarpmapsmapsstracevalgrind- 语言运行时自带工具
解决大量问题。
42. 内存优化真正值得优先做什么
42.1 让热点数据留在内存
这是一切缓存优化的根本。
热点数据反复从磁盘读取,性能一定差。
可以利用:
- Page Cache;
- 应用内存缓存;
- Redis 等外部缓存;
- 数据库 Buffer Pool。
但缓存一定要有边界和淘汰策略。
42.2 减少无意义的动态内存分配
高频:
1 | |
可能带来:
- 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 | |
进程排序
1 | |
进程内存
1 | |
Page Cache / Buffer
1 | |
文件缓存
1 | |
Page Fault / 进程趋势
1 | |
Swap
1 | |
NUMA
1 | |
OOM
1 | |
系统调用
1 | |
内存泄漏
1 | |
或:
1 | |
44. 一套更适合软件开发者记忆的内存性能模型
可以把整个内存篇压缩成四层。
第一层:地址
1 | |
这一层决定:
- 地址空间;
- 页表;
- TLB;
- Page Fault;
- HugePage。
第二层:分配
1 | |
这一层决定:
- VIRT 与 RSS 为什么不一样;
- 内存碎片;
- 动态分配开销;
- 泄漏。
第三层:缓存与回收
1 | |
这一层决定:
- Buffer/Cache;
- LRU;
- Dirty Page;
- Swap;
- kswapd;
- Watermark。
第四层:故障处理
1 | |
这一层决定线上排查方法。
45. 最后:不要把“工具”当知识体系,把“关系”记下来
Linux 内存相关工具很多,但它们其实只是在回答不同层次的问题。
free 回答:
1 | |
vmstat/sar 回答:
1 | |
top/pidstat 回答:
1 | |
pmap/smaps 回答:
1 | |
cachestat/cachetop 回答:
1 | |
memleak/valgrind 回答:
1 | |
zoneinfo/numactl 回答:
1 | |
dmesg 回答:
1 | |
真正稳定的分析方法可以归纳成一句话:
先看系统是否真的有内存压力,再看压力来自 Cache、进程常驻集、内存泄漏还是 Swap;确认类型后,沿着指标找到进程,再沿着进程找到地址空间、系统调用和代码。
只要这条主线还在,即使换了 Linux 版本、换了工具、换了语言运行时,也不会因为某个命令不存在就失去分析能力。