某一年,操作系统监控突然告警内存不足,运维人员分析发现MySQL占用了35GB虚拟内存!而系统的总共内存只有32GB,是内存问题么?淡定的看一眼`top`输出,VIRT栏显示35GB,RES栏只有12GB。“别慌,这是正常现象。”那么问题来了:为什么一个进程可以拥有远超物理内存的虚拟内存?每个进程都觉得自己独占4GB(32位)甚至128TB(64位)的空间,这个“内存幻觉”是如何实现的?
还记得上周处理的MySQL OOM问题吗?
$ top -p $(pidof mysqld)
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 mysql 20 0 35.2g 12.3g 2.1g S 12.3 4.6 123:45.67 mysqld
VIRT = 35.2GB(虚拟内存)
RES = 12.3GB(物理内存)
业务同学的第一反应:“虚拟内存都35GB了,是不是快爆了?”
我的回答:“恰恰相反,这正是虚拟内存的设计精妙之处。”
核心认知:VIRT大不代表物理内存占用高。MySQL的35GB虚拟内存中,大部分是映射的文件(表空间、日志等),但真正加载到物理内存的只有频繁访问的热数据(12.3GB)。
在没有虚拟内存的远古时代(想想DOS),程序员面临的是地狱级难题:
| 内存碎片 | ||
| 进程隔离 | ||
| 共享库 | ||
| 内存超售 |
一句话总结:虚拟内存让每个进程都活在自己的“幻想世界”中,而操作系统就是那个维护这个幻想的“造梦师”。
地址转换是计算机系统中最频繁的操作。假设一个3GHz的CPU每秒执行30亿条指令,每条指令都可能访问内存,那么每秒至少有30亿次地址转换。全球数十亿台设备,每年进行的地址转换次数是个天文数字。
这个“翻译工作”是如何完成的?
虚拟地址 (32位示例)
┌───────────────┬───────────────┐
│ 页号 (20位) │ 页内偏移 (12位)│
└───────────────┴───────────────┘
│ │
▼ │
┌─────────┐ │
│ 页表 │ │
│ 虚拟页→ │ │
│ 物理帧 │ │
└────┬────┘ │
│ │
▼ ▼
┌───────────────┬───────────────┐
│ 帧号 (20位) │ 页内偏移 (12位)│
└───────────────┴───────────────┘
物理地址
每个页表项(PTE)都是64位,但真正存储物理地址的只有一部分,剩下的都是“元数据”:
# x86-64页表项格式(可视化版)
┌─────┬─────┬──┬────┬─┬─┬────┬───┬───┬───┐──┐
│ PFN │保留 │G │PAT │D│A│PCD │PWT│U/S│R/W│P │
└─────┴─────┴──┴────┴─┴─┴────┴───┴───┴───┴──┘
51:12 11:9 8 7 6 5 4 3 2 1 0
这些标志位决定了页面的命运:
| P位 | |||
| A位 | |||
| D位 | |||
| U/S位 |
运维启示:/proc/meminfo中的PageTables就是所有进程页表占用的内存。如果一个进程的VmPTE异常大,可能存在问题:
$ grep VmPTE /proc/[0-9]*/status | sort -k2 -nr | head -5
/proc/1234/status:VmPTE: 123456 kB
/proc/5678/status:VmPTE: 98765 kB
如果使用最简单的线性页表,32位系统需要:
4GB / 4KB = 1,048,576 个页表项
每个页表项 4 字节 → 页表大小 = 4MB
每个进程都需要4MB页表 → 100个进程就需要400MB
更致命的是:大部分页表项是空的(进程只用了少量内存)。这就像为每个家庭准备一个覆盖全城的电话本,99%的号码都是空号。
现代64位系统使用四级页表,就像图书馆的索引系统:
虚拟地址 (48位)
┌─────────┬─────────┬─────────┬─────────┬────────────┐
│ PML4 (9)│ PDP (9) │ PD (9) │ PT (9) │ Offset(12) │
└─────────┴─────────┴─────────┴─────────┴────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 楼层指南 │ │ 区域指南 │ │ 书架指南 │ │ 书架 │ → 找到书
└─────────┘ └─────────┘ └─────────┘ └─────────┘
PML4表 PDP表 页目录 页表
优势:只为实际使用的内存范围分配页表。一个只用了1GB内存的进程,可能只需要几十KB的页表空间。
页表遍历需要多次内存访问,是个昂贵的操作。TLB就是页表的硬件缓存,它的命中率直接影响内存访问性能。
# 用perf查看TLB缺失率
$ perf stat -e dTLB-loads,dTLB-load-misses -p $(pidof mysqld) sleep 10
Performance counter stats for process id '1234':
10,234,567,890 dTLB-loads
45,678,901 dTLB-load-misses # 缺失率 0.45%
# 缺失率计算公式:
# TLB Miss Rate = TLB Misses / TLB Accesses
# 理想值 < 1%,如果 > 5% 说明有性能问题
一个2MB的大页可以覆盖512个4KB小页。这意味着:
# 查看大页使用情况
$ grep -E "HugePages|PageTables" /proc/meminfo
HugePages_Total: 1024 # 2MB大页的数量
HugePages_Free: 512 # 空闲大页
PageTables: 12345 kB # 页表占用内存
# 启用透明大页(对应用透明)
$ echo always > /sys/kernel/mm/transparent_hugepage/enabled
# Java应用使用大页(性能提升5-20%)
java -XX:+UseLargePages -XX:+UseTransparentHugePages -jar app.jar
当进程访问的虚拟页不在物理内存中时,CPU触发缺页异常(Page Fault)。
# 查看缺页统计
$ grep -E "pgfault|pgmajfault" /proc/vmstat
pgfault 1234567890 # 总缺页次数
pgmajfault 123456 # 主要缺页次数(需要磁盘I/O)
# 查看进程缺页情况
$ ps -o pid,min_flt,maj_flt,cmd -p $(pidof nginx)
PID MINFLT MAJFLT CMD
1234 456789 123 nginx: worker process
三种缺页类型:
| 次要缺页 | |||
| 主要缺页 | 需要关注! | ||
| 无效缺页 |
假设磁盘延迟10ms,每秒100次主要缺页:
额外延迟 = 100 × 10ms = 1000ms = 1秒
相当于一个CPU核心完全被I/O占用
排查命令:
# 实时监控缺页
$ pidstat -p $(pidof mysqld) -r 1
10:30:01 UID PID minflt/s majflt/s VSZ RSS %MEM
10:30:02 999 1234 123.45 2.34 35.2g 12.3g 4.56
# majflt/s > 0 就需要警惕了!
当你以为fork()复制了整个进程时,真相是:它几乎什么都没复制。
// fork()的COW魔法
pid_t pid = fork();
if (pid == 0) {
// 子进程:此时和父进程共享所有物理页
data = 42; // 首次写入 → 触发COW
// 内核才真正复制页面
}
COW的过程:
# 测试COW开销
$ cat cow_test.c
#include <unistd.h>
#include <stdlib.h>
#define SIZE (100 * 1024 * 1024) // 100MB
int main() {
char *p = malloc(SIZE);
// 初始化内存,触发物理页分配
for (int i = 0; i < SIZE; i += 4096)
p[i] = 1;
if (fork() == 0) {
// 子进程:全部写入,触发大量COW
for (int i = 0; i < SIZE; i += 4096)
p[i] = 2;
} else {
wait(NULL);
}
return 0;
}
$ time ./cow_test
real 0m0.234s # 100MB的COW只用了0.23秒!
监控COW:
$ grep copy_page /proc/vmstat
copy_page 456789 # 页面复制次数
// 传统文件读取:两次拷贝
char buf[4096];
read(fd, buf, 4096); // 磁盘 → 内核缓冲区 → 用户缓冲区
// mmap文件读取:零拷贝
char *addr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
char first = addr[0]; // 缺页时直接从磁盘到用户空间
mmap的优势:
$ cat /proc/$(pidof mysqld)/maps | grep ".ibd"
7f1234567000-7f1245678000 rw-s 00000000 08:01 1234567 /var/lib/mysql/ibdata1
# rw-s: 读写、共享、私有映射
# 00000000: 文件偏移
# 1234567: inode号
# 查看交换使用
$ swapon --show
NAME TYPE SIZE USED PRIO
/dev/sda2 partition 8G 2.1G -2
$ vmstat 1
procs -----------memory---------- ---swap--
r b swpd free buff cache si so
0 0 2.1G 2.5G 123M 16G 0 0 # si=0, so=0 理想状态
0 1 2.1G 1.2G 123M 16G 123 45 # si>0, so>0 内存紧张!
关键指标:
如果si/so持续非零,说明物理内存严重不足。
# 查看哪些进程在大量使用交换
$ for file in /proc/*/status ; do
awk '/VmSwap|Name/{printf $2 " " $3}END{print ""}'$file 2>/dev/null
done | sort -k2 -n -r | head -10
mysqld 123456 kB
java 98765 kB
redis 12345 kB
现象:大促期间,交易系统响应变慢,监控显示频繁Full GC。
排查步骤:
# 1. 确认内存状态
$ free -h
total used free shared buff/cache available
Mem: 31G 28G 0.5G 1.2G 2.5G 1.8G
Swap: 23G 15G 8.0G
# 可用内存只剩1.8G,交换用了15G!
# 2. 查看缺页统计
$ grep pgfault /proc/vmstat
pgfault 1234567890
pgmajfault 9876543 # 主要缺页很高!
# 3. 定位问题进程
$ ps aux --sort=-vsz | head -5
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
app 12345 89.3 45.6 25.3g 14.2g ? SLl 10:23 123:45 java
# 4. 分析Java堆外内存
$ jcmd 12345 VM.native_memory summary
Native Memory Tracking:
Total: reserved=25.3GB, committed=14.2GB
- Java Heap: reserved=12GB, committed=12GB
- Class: reserved=2GB, committed=1.5GB
- Thread: reserved=1GB, committed=512MB
- Code: reserved=512MB, committed=256MB
- GC: reserved=1GB, committed=512MB
- Compiler: reserved=256MB, committed=128MB
- Internal: reserved=8GB, committed=1GB # 异常!堆外内存泄漏
# 5. 检查透明大页配置
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never # 开启了透明大页,可能引起延迟抖动
解决方案:
# 1. 临时关闭透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 2. 降低swappiness
echo 10 > /proc/sys/vm/swappiness
# 3. JVM参数优化
java -Xms12g -Xmx12g \
-XX:+UseG1GC \
-XX:+UseLargePages \
-XX:+UseTransparentHugePages \
-XX:MaxDirectMemorySize=2g \
-jar app.jar
# 4. 长期:增加物理内存或优化堆外内存使用
PageTables是页表占用的内存| PageTables | |||
| majflt/s | |||
| TLB缺失率 | |||
| si/so | |||
| vm.swappiness |
虚拟内存是操作系统最伟大的抽象之一。它让每个进程都活在自己的“幻想世界”中,而运维工程师的职责就是确保这个幻想不会因物理资源的限制而破灭。
当你的Java进程报出OOM时,不要惊慌,按照本文的思路一步步排查:先看缺页统计,再看交换使用,最后定位到具体进程的内存分布。你会发现,虚拟内存这个“魔术”其实有迹可循。