4、内存性能分析:内存使用率、Swap、缺页中断、内存泄漏检测
内存,说白了就是系统的「工作台」。你工作台越大,能同时摊开的东西就越多。但工作台再大,也架不住东西乱放、或者有人偷偷往上面堆垃圾不收拾。
我做性能调优这些年,碰到的内存问题比CPU问题多得多。CPU不够用,顶多是慢;内存出问题,系统直接给你脸色看——OOM Killer出来杀进程,那叫一个干脆。
今天咱们就把内存分析这四块硬骨头啃下来:使用率怎么看、Swap到底该不该关、缺页中断在暗示什么、以及怎么揪出内存泄漏这个老狐狸。
4.1 内存使用率:别被数字骗了
很多人一看到内存用了80%,就慌了。其实没必要。Linux的内存管理哲学是「闲着也是闲着,不如拿来缓存」。你看到的used,里面很大一部分是buff/cache。
我习惯用这个命令看真实的内存压力:
free -h
total used free shared buff/cache available
Mem: 31Gi 12Gi 1.2Gi 1.5Gi 18Gi 18Gi
Swap: 2.0Gi 256Mi 1.8Gi
关键看哪个?available。它才是系统真正能分给新进程的内存。你看上面,used虽然12G,但available还有18G,说明内存其实很宽裕。
watch -n 1 free -h 持续观察。如果available持续低于总内存的10%,就该警惕了。
还有一个更细粒度的工具——/proc/meminfo。这里面藏着很多宝贝:
cat /proc/meminfo | grep -E "^(MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree|Dirty|Writeback)"
Dirty和Writeback这两个值很有意思。它们表示「等着写回磁盘的脏页」。如果Dirty一直很大,说明磁盘IO是瓶颈,内存再大也没用。
4.2 Swap:该不该关?
这个问题我被人问过不下五十次。我的回答是:别一刀切。
Swap是磁盘上划出来的一块空间,假装自己是内存。进程长时间不用的内存页,会被换到Swap里。这叫「换出」。等要用的时候,再换回来。
但磁盘比内存慢几个数量级。一旦系统频繁换入换出,你就等着卡死吧。
怎么看Swap的使用情况?
vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 262144 1234567 234567 8901234 0 0 12 34 567 890 5 2 93 0 0
重点关注si(swap in)和so(swap out)。这两个值如果持续大于0,说明内存真的不够用了。
关于swappiness这个参数,我建议这样设:
| 场景 | 推荐值 | 理由 |
|---|---|---|
| 普通服务器 | 10-30 | 留点余地,防止OOM |
| 数据库/缓存 | 1-10 | 延迟敏感,尽量少换页 |
| 桌面/开发机 | 60 | 默认值,兼顾响应和内存效率 |
修改方法很简单:sysctl vm.swappiness=10,想永久生效就写到/etc/sysctl.conf里。
4.3 缺页中断:性能的隐形杀手
缺页中断,英文叫Page Fault。进程访问的内存页不在物理内存里,就会触发这个中断。操作系统得去磁盘上把数据读进来。
缺页分两种:
- 轻微缺页:页在内存里,但页表还没建立。这种很快,基本没影响。
- 严重缺页:页真的在磁盘上。这种就慢了,一次可能要几毫秒甚至几十毫秒。
用sar -B 1可以看缺页情况:
sar -B 1
Linux 5.4.0 (myserver) 01/01/2024 _x86_64_ (8 CPU)
10:00:01 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
10:00:02 1024.00 2048.00 1234.00 2.00 5678.00 0.00 0.00 0.00 0.00
看majflt/s,就是每秒的严重缺页次数。这个值如果持续大于0,说明你的程序在频繁访问磁盘上的数据。
实战经验: 我调过一个Java应用,GC日志显示一切正常,但响应时间就是忽高忽低。用sar一看,majflt/s时不时跳到50多。后来发现是堆内存设得太小,对象被频繁换到Swap里了。把堆内存从4G调到8G后,majflt/s直接降到了0,响应时间也稳了。
还有一个工具叫perf,可以精确追踪是哪个函数触发了缺页:
perf record -e page-faults -ag -- sleep 10
perf report
嗯,这个命令会采样10秒内的所有缺页事件,然后告诉你哪些代码路径最「缺页」。我一般用它来定位到底是哪个模块在吃内存。
4.4 内存泄漏检测:揪出那个偷内存的家伙
内存泄漏,说白了就是「申请了内存,用完不还」。C/C++程序最容易犯这个毛病。Java有GC,但GC本身也可能泄漏——比如往一个静态Map里不停塞数据,永远不清理。
怎么发现内存泄漏?我一般分三步走:
4.4.1 第一步:看趋势
用top或者htop看进程的RES(常驻内存)是不是一直在涨。如果重启后正常,跑几天又涨上去了,十有八九是泄漏。
我习惯用这个脚本持续记录:
while true; do
ps -p <PID> -o pid,rss,%mem --no-headers >> /tmp/mem.log
sleep 60
done
跑个几小时,把数据导出来画个图,一目了然。
4.4.2 第二步:用专业工具
对于C/C++程序,我推荐Valgrind的memcheck工具:
valgrind --tool=memcheck --leak-check=full ./your_program
它会告诉你每一块泄漏的内存在哪里申请的,甚至能精确到源码行号。不过要注意,程序跑起来会慢很多,适合在测试环境用。
对于Java程序,我一般用jmap和MAT(Memory Analyzer Tool):
# 先dump堆内存
jmap -dump:live,format=b,file=/tmp/heap.hprof <PID>
# 然后用MAT打开heap.hprof,看哪些对象占用了最多内存
jstat -gcutil <PID> 1000看GC情况。如果Full GC越来越频繁,老年代持续增长,基本可以断定有泄漏。
4.4.3 第三步:定位代码
工具告诉你泄漏了,但怎么修?我分享一个真实案例。
有一次,一个Go写的网关服务,内存每12小时涨1G。我用pprof抓了堆内存:
import _ "net/http/pprof"
// 然后访问 /debug/pprof/heap?debug=1
发现有个map一直在增长,里面存的是HTTP请求的响应体。查代码发现,有个协程处理完请求后,忘了调用resp.Body.Close()。就这么一行代码,导致连接和内存都泄漏了。
4.5 总结一下
内存性能分析,说白了就是回答四个问题:
- 够不够用? —— 看available,别被used骗了
- 有没有在换页? —— 看si/so,持续大于0就要加内存了
- 缺页严重吗? —— 看majflt,定位到具体代码路径
- 有没有泄漏? —— 看趋势,用工具dump,找到那个不释放的「钉子户」
你想想看,内存问题其实比CPU问题好定位。因为它的症状很明确——要么涨,要么换。只要你有耐心,一步步排查,总能找到根因。
下一章,咱们聊聊磁盘IO。那又是一个全新的世界,嗯,到时候再细说。