内存问题是最隐蔽的性能杀手。它不像CPU瓶颈那样明显——内存不足时,系统不会直接报错,而是悄悄地把数据换到磁盘,然后一切都变慢了。等你想起来的时候,可能已经在SWAP的深渊里了。
第4章 内存性能分析:逃离SWAP的深渊
4.1 故事:那个"内存泄漏"的周末
周五下班前,运维工程师小李收到告警:某推荐服务的内存使用率从60%慢慢爬到了95%。
"内存泄漏。"他断定,然后重启了服务。内存回到40%,他放心地回家了。
周六早上,告警又来了。这次爬得更快——4小时就到了90%。
小李加了班,用jmap做了heap dump,用MAT分析了一整天。结论:没有泄漏。对象的引用链都是正常的,只是缓存确实需要这么多内存。
"那就是业务增长导致的正常增长。"他给总监发了报告,申请加内存。
周一,加了64GB内存。周二,告警又来了。
资深工程师老周接手。他打开vmstat:
vmstat 1 10
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 51200 2048 1024 45678 0 0 0 0 23 456 5 2 93 0 0"等等,"老周注意到swpd(已用SWAP)是51200KB,但si/so(SWAP进出)是0。
"SWAP用了,但没有换入换出?这不合理。"
他继续查:
cat /proc/swaps
Filename Type Size Used Priority
/dev/dm-0 partition 8388604 51200 -2
# 查看哪些进程在用SWAP
for file in /proc/*/status; do
awk '/VmSwap|Name/{print $2, $3}' $file 2>/dev/null
done | grep -B1 kB | head -20发现一个定时任务进程占用了大部分SWAP——那是一个每晚运行的数据分析任务,白天应该结束的,但代码里有个死循环,进程没退出。
根因:不是内存泄漏,是僵尸进程长期占用SWAP空间。
修复:找到并终止僵尸进程 + 优化进程生命周期管理。内存问题消失。
教训:SWAP使用≠内存不足,先看谁在占,再看有没有换入换出。
4.2 内存层次结构
graph TD
A["CPU寄存器
~0ns"] --> B["L1缓存
~1ns 32-64KB"]
B --> C["L2缓存
~10ns 256KB-1MB"]
C --> D["L3缓存
~20-50ns 8-64MB"]
D --> E["主内存
~100ns 16-512GB"]
E --> F["SWAP/磁盘
~10ms 无限"]
style A fill:#e8f5e9
style F fill:#ffebee关键洞察:相邻层级速度差约10倍。从内存到磁盘,延迟暴涨10万倍。
4.3 虚拟内存与页表
地址翻译过程
sequenceDiagram
participant CPU as CPU
participant MMU as MMU/TLB
participant PT as "页表"
participant Mem as "物理内存"
CPU->>MMU: 虚拟地址
MMU->>MMU: 查TLB
alt TLB命中
MMU-->>CPU: 物理地址(快)
else TLB未命中
MMU->>PT: 查页表
PT-->>MMU: 页表项
MMU->>MMU: 更新TLB
MMU-->>CPU: 物理地址(慢)
end
CPU->>Mem: 访问物理地址大页(HugePage)的价值
# 标准4KB页
虚拟地址空间:48位 = 256TB
页表级数:4级(PGD→PUD→PMD→PTE)
页表项数量:256TB / 4KB = 64G项
TLB压力:巨大
# 2MB大页
页表项数量:256TB / 2MB = 128M项(减少500倍)
TLB命中率:显著提升
# 1GB大页
页表项数量:256TB / 1GB = 256K项
TLB:几乎完全缓存应用场景:
- 数据库(MySQL、PostgreSQL、Oracle):启用大页,减少TLB miss
- JVM:
-XX:+UseLargePages - DPDK:必须用大页
4.4 Page Cache:性能的双刃剑
Page Cache是什么?
graph LR
A["应用程序"] -->|read| B["Page Cache"]
B -->|"命中"| C["直接返回
~100ns"]
B -->|"未命中"| D["磁盘读取
~10ms"]
D --> E["写入Page Cache"]
E --> C
F["应用程序"] -->|write| G["Page Cache"]
G -->|"异步刷盘"| H["磁盘"]
style C fill:#e8f5e9
style D fill:#ffebee查看Page Cache
# 查看缓存命中率
cat /proc/vmstat | grep -E "pgpgin|pgpgout|pswpin|pswpout"
# 查看文件缓存
pcstat /path/to/file
# +--------+------------+------------+-----------+---------+
# | Name | Size | Pages | Cached | Percent |
# | | | | | |
# |--------+------------+------------+-----------+---------+
# | data | 1.0 GiB | 262144 | 262144 | 100.00 |
# +--------+------------+------------+-----------+---------+
# 查看哪些文件在缓存中
vmtouch -v /path/to/directory清理Page Cache(测试用)
# 仅清理页缓存
echo 1 > /proc/sys/vm/drop_caches
# 清理页缓存和inode/dentry
echo 2 > /proc/sys/vm/drop_caches
# 清理所有缓存(包括脏页,危险!)
echo 3 > /proc/sys/vm/drop_caches4.5 SWAP:最后的救命稻草,也是最坏的性能杀手
SWAP触发路径
graph TD
A["进程申请内存"] --> B{空闲内存够?}
B -->|"是"| C["直接分配"]
B -->|"否"| D["回收Page Cache"]
D --> E{回收够?}
E -->|"是"| C
E -->|"否"| F["触发SWAP"]
F --> G["将匿名页写入SWAP"]
G --> H["进程获得内存"]
H --> I["但访问被换出的页时
需要读回磁盘"]
style C fill:#e8f5e9
style I fill:#ffebeeSWAP分析命令
# 查看SWAP总体
free -h
# total used free shared buff/cache available
# Mem: 128Gi 80Gi 2.0Gi 1.2Gi 46Gi 44Gi
# Swap: 8.0Gi 512Mi 7.5Gi
# 查看谁在占SWAP(BCC工具)
sudo swapon-bpfcc
# 或手动遍历
for pid in $(ls /proc | grep -E '^[0-9]+$'); do
if [ -f /proc/$pid/status ]; then
name=$(grep ^Name: /proc/$pid/status | awk '{print $2}')
swap=$(grep ^VmSwap: /proc/$pid/status | awk '{print $2 $3}')
if [ "$swap" != "0kB" ]; then
echo "$pid $name $swap"
fi
fi
done | sort -k3 -n -r | head -20swappiness参数
# 查看当前值
cat /proc/sys/vm/swappiness
# 默认值60
# 含义:0-100,表示内核倾向于用SWAP的程度
# 0:除非内存完全耗尽,否则不用SWAP
# 100:积极使用SWAP
# 建议:
# 数据库服务器:10-30(尽量少用SWAP)
# 桌面系统:60(平衡)
# 开发测试机:100(无所谓)4.6 内存分配器
三种主流分配器对比
| 分配器 | 特点 | 适用场景 |
|---|---|---|
| ptmalloc (glibc默认) | 兼容性好,多线程有锁竞争 | 通用 |
| jemalloc (Facebook) | 减少碎片,线程缓存 | 高并发、长进程 |
| tcmalloc (Google) | 快速,小对象优化 | 高频率分配释放 |
# 查看当前使用的分配器
ldd /usr/bin/java | grep malloc
# 切换到jemalloc
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so
# MySQL使用jemalloc
mysqld_safe --malloc-lib=jemalloc4.7 内存泄漏检测
工具链
# ===== valgrind(最全面但最慢) =====
valgrind --tool=memcheck --leak-check=full ./program
# 输出:
# LEAK SUMMARY:
# definitely lost: 1,234 bytes in 56 blocks
# indirectly lost: 2,345 bytes in 78 blocks
# possibly lost: 456 bytes in 12 blocks
# still reachable: 67,890 bytes in 1,234 blocks
# ===== address sanitizer(编译时加入,速度快) =====
gcc -fsanitize=address -g ./program.c
./program
# ===== bcc工具(运行时,无需修改代码) =====
sudo memleak-bpfcc -p $(pidof java) 10
# 每10秒输出新增未释放的内存分配
# ===== Java专用 =====
jmap -dump:live,format=b,file=heap.hprof $(pidof java)
# 用Eclipse MAT分析
jcmd $(pidof java) VM.native_memory summary
# 查看JVM各区域内存使用4.8 NUMA:多CPU时代的内存陷阱
NUMA架构
graph TD
subgraph 节点0
CPU0["CPU 0-3"]
Mem0["本地内存"]
end
subgraph 节点1
CPU1["CPU 4-7"]
Mem1["本地内存"]
end
subgraph 节点2
CPU2["CPU 8-11"]
Mem2["本地内存"]
end
subgraph 节点3
CPU3["CPU 12-15"]
Mem3["本地内存"]
end
CPU0 -.本地访问.-> Mem0
CPU0 -.远程访问
慢~2倍.-> Mem1
CPU1 -.本地访问.-> Mem1
CPU1 -.远程访问.-> Mem2NUMA分析
# 查看NUMA拓扑
numactl --hardware
# 查看进程NUMA分布
numastat -p $(pidof java)
# 绑定进程到本地NUMA节点
numactl --cpunodebind=0 --membind=0 ./program
# 让内核自动均衡(默认)
echo 1 > /proc/sys/kernel/numa_balancing性能影响:跨NUMA访问比本地慢1.5-2倍。数据库等内存密集型应用必须关注NUMA绑定。
4.9 本章总结
mindmap root((内存
性能分析)) 层次结构 寄存器→L1→L2→L3→内存→磁盘 相邻层级差10倍 虚拟内存 页表翻译 TLB缓存 大页优化 Page Cache 命中率是关键 清理命令 SWAP 尽量避免 swappiness调优 分配器 ptmalloc/jemalloc/tcmalloc 泄漏检测 valgrind/asan/bcc NUMA 本地优先 绑定优化
核心要点:
- TLB miss是内存性能隐形杀手——考虑大页
- Page Cache决定文件I/O速度——监控命中率
- SWAP用了不代表在换入换出——先看谁在占
- 内存分配器选择对高并发应用影响很大
- NUMA架构下,本地内存访问快2倍——合理绑定
"内存是距离CPU最近的存储,也是最容易被忽视的性能瓶颈。"