
我这篇就不搞什么“故事开头”“面试八股”,直接开干。 虚拟内存这个东西,大家都听过,但真到线上一出 “内存爆了”“swap 狂飙”“某进程 OOM 了”,很多人第一反应还是重启。 重启不是不行,就是丢人,而且问题也没解决。
这次我想把自己这几年在 Linux 和 Windows 上折腾虚拟内存踩过的坑掏一遍,按“问题—机制—实战”这么个路子说,说着说着可能会跑题,不过也算是我平时排障的真实思路。
先把话说明白:虚拟内存这玩意,本质就是用一层“中间地址”把进程看到的地址和真实物理内存隔开。
为啥要隔开?
Linux 也好,Windows 也好,都是干这几件事,只是细节上差别挺多。 运维日常遇到的那些 “page cache 占满”“commit charge 爆了”“swap 抖动” 之类现象,全和这些细节有关。
当年 32 位时代,段式、页式这些词经常一起出现。简单说:
现在 64 位时代,大家更偏向统一平坦地址空间,但历史包袱还在,行为上还是能看出差异。
Windows 下一个虚拟地址要经过两步:
对我们运维来说,这里有两个点要记:
Linux 这边就简单粗暴很多:
为什么我们 top 里经常能看到进程虚拟内存动辄几个 G,甚至几十 G? 就是因为 Linux 在虚拟地址这块特别“豪横”,管你物理上有多少,先映射再说,真正用到时再分配物理页。
这也导致 Linux 上经常出现那种:
VIRT 巨大,RES 不大;Windows 上也有类似概念,只是表现形式不太一样,后面讲。
很多人看 Linux 的 /proc/<pid>/maps 或者 Windows 的 VMMap 截图,会发现地址空间是块状的,用户区、内核区、堆、栈、mmap 区,挤得满满的。
32 位 CPU 能寻址的最大空间是 4G,这 4G 还要用户和内核分。
常见配置:
/3GB 开关,可以把用户空间扩大到 3G,内核 1G,但稳定性要打折;这事对运维有什么影响? 比如老旧 32 位系统上跑 Java / Oracle,这种进程自己就想要好几个 G 堆,结果地址空间都不够,连启动都费劲。
我以前遇到一个 Windows 2003 + 32 位 Oracle 的老系统,某天业务加了点数据,实例重启直接起不来,报错内存不足,最后一看,纯属地址空间撑爆了,物理内存还剩一堆。 那次之后我对“虚拟地址空间”这个概念算是记死了。
64 位下的理论空间大到离谱,不过 OS 和硬件都会限制几级页表,实际有效的也足够夸张。
典型配置:
有同事看到 Linux 上 VIRT 动辄几十 G 就慌,我的经验是:
RES(常驻集)还合理,swap 不怎么动,就别太紧张;commit(overcommit)和 swap 抖动,而不是单纯虚拟地址数值。虚拟内存说来说去,离不开一个关键词:换页。 物理内存不够时,OS 把不常用的页写到磁盘,再需要时再读回来,这个过程就是大家经常骂的 “swap”。
Windows 用的是页面文件 pagefile.sys:
实际排障时,我比较关注:
Commit charge 接近 Commit limit 时,系统就危险了;我有一次帮人看一台 Windows 文件服务器,32G 内存,结果 pagefile set 成了 1G,某个备份进程一跑,commit 立刻顶满,系统开始疯狂杀进程,连 RDP 都连不上。 最后把 pagefile 设置为自动,commit limit 上去了,系统就稳多了。
Linux 更灵活:
swapfile),用 mkswap + swapon 搞定。几个点我踩过的坑:
生产里我一般:
虚拟内存除了用来“救急”,还有一块经常被忽略,就是内存映射文件。
Windows 提供了 CreateFileMapping / MapViewOfFile 这一套:
实战里:
如果你在 Windows 上看到一个进程 Working Set 很大,但磁盘读写并不多,很可能就是用了大规模的 memory mapped file。
Linux 下就是 mmap:
void* addr = mmap(NULL, length, PROT_READ | PROT_WRITE,
MAP_SHARED, fd,);运维侧可以从 /proc/<pid>/maps 里看到映射的文件路径:
.so 也是以映射方式加载的,读写共享。这块对我们排障的重要意义在于:
RES 里面包含了这些映射文件占的页;RES 就慌,要看 RSS、anon 等更细的指标。物理内存有限,哪些页留着,哪些丢掉? 这里就是不同操作系统“性格”的差异所在。
Windows 里有个概念叫 Working Set:
任务管理器里看到的 “Working Set (Memory)” 基本就是这个概念。
这会带来一些有意思的现象:
Linux 常见说法是“LRU”(Least Recently Used),实际上现在内核里的算法比纯 LRU 复杂多了,不过我们可以这么粗略理解:
此外还有:
kswapd 负责内存回收,必要时触发 direct reclaim。我们在监控里经常会看到:
page cache 一路涨,直到内存压力上来,内核开始回收 cache;一旦 you see:
dmesg | grep -i oom出现 OOM killer 日志,说明内核已经开始杀进程自救了。 这个时候再骂“Linux 内存管理垃圾”其实也没啥用,更多是你业务超出了机器承载。
说到虚拟内存,就离不开 TLB(Translation Lookaside Buffer)。
简单粗暴解释:
不同操作系统在处理 TLB 失效(TLB miss)时策略不一样,比如上下文切换时要不要 flush TLB,怎么标记 ASID 等,这些细节对于我们这种运维一般不需要太深入。 不过有个点还是很实在:
这也就是为什么一些数据库、中间件都建议配置 HugePage / Large Page 的原因。
虚拟内存还有一个大用处是内存保护:
Linux 下常见的是 Segmentation fault,Windows 下是 Access Violation。
对我们运维来说:
这块更多是开发要修,但我们至少要知道: 不是所有崩溃都是“内存不足”,有些是“乱写被 OS 当场抓住”。
这里提一个很多新同学都没怎么碰过的概念:高端内存。
主要出现在 32 位 Linux 时代:
原因就是内核空间只有 1G/2G 的那一小块,还要同时映射设备、内核自身、各种缓存,物理内存一多就映射不过来。
如今大多数生产都 64 位了,这事影响不大,不过如果你还在维护一些特别老的系统,这个概念会在 dmesg 里频繁出现。
内存回收这块,是运维最容易感知到系统“性格”的地方。
Linux 的回收链路大致是:
kswapd 定时扫描页,回收不常用的;你可以从这些地方观察:
/proc/meminfo 里的 SwapCached, Dirty, Writeback, Active, Inactive 等;vmstat 1 看 si/so(swap in/out)、cs(上下文切换)、wa(IO 等待);dmesg 里 OOM 日志。有一次某平台的批处理任务跑着跑着,突然一批服务集体挂,监控上看 CPU 并不高,就是 load 高得离谱,磁盘 IO 爆,最后查下来: 那段时间大量任务堆积,内存吃满了,系统开始疯狂 reclaim + swap,最后扛不住 OOM,把几个大进程干掉了。 那次之后我们在批处理集群上专门做了内存水位限流和 swap 监控。
Windows 这边的逻辑则是:
任务管理器 / 资源监视器 / RAMMap 这些工具,其实就是把这些列表可视化出来。 比如 RAMMap 里 Standby List 特别大时,系统整体还算健康; 一旦 Standby 也被吃光,Free 几乎为 0,而且 pagefile 写得飞快,那就是真的紧张。
这里随手列几个我见得比较多的坑,算是和大家共勉。
很多刚上手的同学,装完 Linux 第一件事就是关 swap,理由是“swap 会拖慢系统”。
结果:
我现在的习惯:
oom_score_adj 调低被杀的优先级;Linux 上 free 输出里 cached、buff/cache 很高,其实大多是好事:
我见过有人看到 free 输出里 used 90%+,立刻加 swap 再重启,结果没必要。
真正要担心的是:
buff/cache 被吃掉,available 很低;另外一个经典坑:
这类问题在 Windows、Linux 上都见过。 解决思路无非:
虚拟内存这块,说白了是在软件层面补硬件资源的短板,同时又帮我们做了隔离、安全、性能优化这些事。 Linux 和 Windows 的设计理念不一样,表现出来的各种指标也不完全相同,但只要抓住几个核心点:
很多“看起来玄学”的现象,其实都能解释得清清楚楚。
我这边写的东西比较散,就是平时查问题、踩坑时积累下来的那点体会,能帮你少重启几次机器就算没白写。 以后有机会还会单独拆开,比如专门写一篇“如何系统分析 Linux OOM”和“Windows 内存泄漏排查实战”,就不挤在这一篇了。
如果你看到这里还没睡着,那咱算有点缘分。 欢迎把这篇文章转给身边还在为 “内存爆了怎么办” 头大的同事,也欢迎在评论区说说你碰到过什么奇葩的虚拟内存问题,大家一起交流。
想系统看我后续的运维实践文章,记得关注一下 @运维躬行录,别走丢了。