性能之巅第4章:内存性能分析

📑 目录

内存问题是最隐蔽的性能杀手。它不像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_caches

4.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:#ffebee

SWAP分析命令

# 查看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 -20

swappiness参数

# 查看当前值
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=jemalloc

4.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 -.远程访问.-> Mem2

NUMA分析

# 查看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 本地优先 绑定优化

核心要点

  1. TLB miss是内存性能隐形杀手——考虑大页
  2. Page Cache决定文件I/O速度——监控命中率
  3. SWAP用了不代表在换入换出——先看谁在占
  4. 内存分配器选择对高并发应用影响很大
  5. NUMA架构下,本地内存访问快2倍——合理绑定

"内存是距离CPU最近的存储,也是最容易被忽视的性能瓶颈。"