内存问题是最隐蔽的性能杀手。它不像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最近的存储,也是最容易被忽视的性能瓶颈。"