Linux 内存管理深潜
"内存又满了,是不是泄漏了?"—— Linux 内核会主动把空闲内存拿去做 Buffer 和 Cache,这不是泄漏,这是特性。关键是分清"已用内存"和"真正被应用程序占用的内存"。
核心概念
Buffer 和 Cache 的本质区别
| 概念 | 作用 | /proc/meminfo 字段 | 释放条件 |
|---|---|---|---|
| Buffer | 块设备元数据缓存(superblock、inode bitmap 等),加速磁盘元数据操作 | Buffers |
内存紧张时可回收 |
| Page Cache | 文件页缓存,把磁盘文件内容缓存到内存,加速读写 | Cached |
干净页直接回收,脏页需回写后回收 |
| Slab | 内核对象缓存(dentry、inode、网络缓冲区等) | SReclaimable / SUnreclaimable |
可回收部分自动释放 |
Linux 和 Windows 内存管理有本质区别:Windows 显示"已用内存"不含缓存,Linux 的
free列是真正空闲的,available才是应用可用的。
free -h 解读
$ free -h
total used free shared buff/cache available
Mem: 31Gi 8.2Gi 2.1Gi 1.3Gi 21Gi 22Gi
Swap: 8.0Gi 1.2Gi 6.8Gi
| 字段 | 含义 | 关键判断 |
|---|---|---|
used |
应用程序 + 内核占用(不含可回收缓存) | 持续增长 → 可能泄漏 |
free |
完全未使用的内存 | 低不一定有问题 |
buff/cache |
Buffer + Page Cache + Slab 可回收 | ✅ 正常行为,会用空闲内存 |
available |
应用实际可申请的内存(含可回收缓存) | ← 这才是真正的可用量 |
⚠️ 不要看
free列,要看available列。free低但available高 → 系统健康,只是缓存用得多。
/proc/meminfo 关键字段
$ cat /proc/meminfo
MemTotal: 32768000 kB # 总物理内存
MemFree: 2200000 kB # 完全空闲
MemAvailable: 23100000 kB # 可用(含可回收)
Buffers: 520000 kB # 块设备元数据缓存
Cached: 16500000 kB # 文件页缓存
SwapCached: 120000 kB # 曾经换出又换回的页
Active: 8500000 kB # 活跃页(最近被访问)
Inactive: 9200000 kB # 不活跃页(优先回收)
Dirty: 32000 kB # 等待回写的脏页
Writeback: 0 kB # 正在回写的页
Slab: 780000 kB # Slab 总大小
SReclaimable: 520000 kB # 可回收 Slab(dentry、inode 缓存)
SUnreclaimable: 260000 kB # 不可回收 Slab
KernelStack: 45000 kB # 内核栈
PageTables: 62000 kB # 页表
快速诊断:
Dirty持续 > 几百 MB → IO 瓶颈,脏页来不及回写SUnreclaimable持续增长 → 可能内核内存泄漏MemAvailable持续下降 → 真实内存压力
Page Cache 机制
Page Cache 是 Linux 最核心的 IO 性能优化之一。读写文件时,内核先把数据缓存在 Page Cache 中,后续访问直接从内存命中:
# 查看 Page Cache 命中率
sar -B 1 5
# pgpgin/s: 每秒从磁盘读入的页数
# pgpgout/s: 每秒写出到磁盘的页数
# fault/s: 缺页次数
# majflt/s: 主缺页(需读磁盘)← 高说明 Cache 命中不足
手动释放 Page Cache(仅测试/紧急情况):
echo 1 > /proc/sys/vm/drop_caches # 释放 Page Cache
echo 2 > /proc/sys/vm/drop_caches # 释放 Slab(dentry/inode)
echo 3 > /proc/sys/vm/drop_caches # 释放两者
⚠️ 生产环境不要轻易 drop_caches,会瞬间增加磁盘 IO 压力。
Slab 内存
Slab 是内核对象缓存,slabtop 可以查看哪些内核结构占用最多:
slabtop -s c | head -20
| 常见 Slab 对象 | 含义 | 异常信号 |
|---|---|---|
| dentry | 目录项缓存 | 频繁 ls / find 大量小文件导致暴涨 |
| inode_cache | inode 缓存 | 同上 |
| kmalloc-* | 通用内核内存分配 | 持续增长 → 内核模块泄漏 |
| proc_inode_cache | /proc inode | 大量 /proc 读取 |
| TCP | TCP 缓冲区 | 大量网络连接 |
SUnreclaimable 持续增长是最常见的内核内存泄漏信号。
内存回收三级机制
第一级:kswapd(后台回收)
内核线程 kswapd 在内存低于 watermark 时被唤醒,异步回收干净页、压缩内存、换出匿名页。
# 查看 kswapd 活动
ps aux | grep kswapd
cat /proc/vmstat | grep -E "kswapd|pgsteal|pgscan"
第二级:Direct Reclaim(直接回收)
当内存分配速度超过 kswapd 回收速度,申请内存的进程自己被阻塞去回收内存。这是性能拐点——应用线程突然变慢。
# 查看 direct reclaim 活动
cat /proc/vmstat | grep allocstall
# allocstall_dma / allocstall_normal 持续增长 → 内存严重不足
第三级:OOM Killer(终极手段)
前两级都跟不上时,OOM Killer 按 oom_score 选择进程杀掉。
# 查看 OOM 事件
dmesg | grep -i "out of memory\|killed process"
# 查看进程 oom_score(越高越容易被杀)
cat /proc/<PID>/oom_score
# 保护关键进程(-1000 完全保护,1000 优先被杀)
echo -1000 > /proc/<PID>/oom_score_adj
cgroup v2 内存控制
在容器/K8s 环境中,cgroup v2 提供精细的内存控制:
# 查看 cgroup 内存限制
cat /sys/fs/cgroup/memory.max # 硬限制(字节),超过触发 OOM
cat /sys/fs/cgroup/memory.high # 软限制,超过触发回收但不杀进程
cat /sys/fs/cgroup/memory.low # 保护阈值,低于此值尽量不回收
# cgroup 内存压力
cat /sys/fs/cgroup/memory.pressure
# some avg10=25.30 avg60=18.72 # 至少一个任务因内存阻塞
# full avg10=5.10 avg60=3.20 # 所有任务都因内存阻塞
关键指标:
memory.current— 当前使用量memory.stat— 详细统计(含 Page Cache、Swap、Kernel 等)nr_throttled不为 0 → 内存被 cgroup 限流
内存泄漏排查工具链
快速判断:真泄漏还是正常缓存?
# 1. 看 available,不是 free
free -h | grep Mem
# 2. available 持续下降 + free 已很低 → 真实压力
watch -n 2 'free -h | grep Mem'
# 3. 检查是否 Swap 在增长
vmstat 1 5 | awk 'NR>2{print "si:",$7,"so:",$8}'
定位泄漏进程
# 按 RSS(物理内存)排序
ps aux --sort=-%mem | head -20
# 更精确:PSS(按共享比例分摊)
smem -s pss -r | head -20
# 观察进程内存趋势
pidstat -r -p <PID> 1 10
深入分析
# /proc/<PID>/smaps — 进程内存映射详情
cat /proc/<PID>/smaps | grep -E "^(Rss|Pss|Shared|Private)"
# /proc/<PID>/smaps_rollup — 汇总版
cat /proc/<PID>/smaps_rollup
# 查看内存映射区域
pmap -x <PID>
eBPF/bpftrace 追踪
# 追踪进程内存分配(谁在申请内存)
bpftrace -e 'tracepoint:kmem:kmalloc {
@[comm] = count(); }'
# 追踪 OOM 事件
bpftrace -e 'kprobe:oom_kill_process {
printf("OOM killer activated, killing %s pid %d\n", str(args->p->comm), args->p->pid); }'
# 追踪 Page Cache 回写延迟
bpftrace -e 'kprobe:balance_dirty_pages {
@usecs = hist(nsecs / 1000); }'
Java/Go 应用专用
# Java:查看 JVM 内存(堆+元空间+Native)
jcmd <PID> VM.native_memory summary
# Go:pprof 内存分析
curl http://localhost:6060/debug/pprof/heap > heap.prof
go tool pprof -http=:8080 heap.prof
常见场景速查
| 现象 | 根因 | 排查入口 |
|---|---|---|
free 很低但 available 很高 |
Page Cache 正常占用 | ✅ 正常,无需处理 |
available 持续下降 |
真实内存压力 | ps 按 %MEM 排序 |
SUnreclaimable 持续增长 |
内核内存泄漏(dentry/inode 泄漏) | slabtop -s c |
Dirty 持续很大 |
IO 瓶颈,脏页来不及回写 | iostat -x 检查磁盘 |
allocstall 持续增长 |
Direct Reclaim,性能拐点 | 扩容或降内存使用 |
| Swap 持续抖动 | 物理内存不足 | free -h + ps 排查 |
| OOM Killer 触发 | 内存严重不足 | dmesg 查看被杀进程 |
监控告警
# 内存可用率(最重要)
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.2
# Swap 使用率
node_memory_SwapFree_bytes / node_memory_SwapTotal_bytes < 0.5
# Direct Reclaim 活动
rate(node_vmstat_allocstall[5m]) > 0
# OOM Kill 事件
increase(node_vmstat_oom_kill[5m]) > 0
# Slab 不可回收持续增长
deriv(node_memory_SUnreclaim_bytes[1h]) > 10485760 # > 10MB/hour
# cgroup 内存压力
container_memory_pressure > 20
关联页面
| 页面 | 关联点 |
|---|---|
| server-performance-four-dimensions | 服务器五维排查总纲(内存为其中一维) |
| redis-memory-optimization | Redis 内存优化(应用层内存管理) |
| linux-kernel-tuning-production | 内核参数调优(vm.swappiness/vm.min_free_kbytes) |
| linux-load-average-guide | Load Average 与 OOM/Swap 的关联排查 |
| linux-disk-io-tuning | 磁盘 IO 调优(脏页回写与 Page Cache 关系) |
| jvm-container-oom-offheap-troubleshooting | JVM 堆外内存(DirectByteBuffer/Metaspace/线程栈)导致容器 OOMKi |
| k8s-capacity-planning-qos-cost-optimization | K8s 容量规划方法论与成本优化 — 从流量到资源预算的完整框架,含 QoS 策略、弹性伸缩协同、落 |
| linux-filesystem-directory-structure-guide | Linux FHS 文件系统目录结构详解 — 含各目录用途、运维排查路径速查、磁盘分区方案 |
| online-troubleshooting-checklist | 四维排查速查清单(CPU/磁盘/内存/网络 + Java 工具 jstack/jmap/jstat/tcpdump) |