- 文档
- 教程
- 知识库
【免费下载链接】CS-Base
图解计算机网络、操作系统、计算机组成、数据库,共 1000 张图 + 50 万字,破除晦涩难懂的计算机基础知识,让天下没有难懂的八股文!🚀 在线阅读:https://xiaolincoding.com
本文基于 CS-Base《图解系统》内存管理章节,围绕经典面试题「在 4GB 物理内存的机器上申请 8G 内存会怎么样」展开。这个问题的答案取决于操作系统位数(32/64 位)、申请后是否实际访问、是否启用 Swap 三个前置条件。读完本文,你将掌握 malloc 虚拟内存语义、缺页中断流程、overcommit_memory参数、Swap 换入换出与 OOM Killer 的完整知识链,并能用ps aux、/proc/sys/vm/overcommit_memory、/proc/<pid>/maps、top等工具亲手复现实验、验证结论。
问题背景:为什么「8G 内存」的答案众说纷纭
在技术社区里,「在 4GB 物理内存的机器上,申请 8G 内存会怎么样?」一直存在争议:有人说会申请失败,有人说可以申请成功。这两种说法都对,也都不完整——在没有前置条件下直接给出答案,就是在耍流氓。
要严谨地回答这个问题,必须先明确三个前置条件:
- 操作系统是 32 位的,还是 64 位的?这决定了进程虚拟地址空间的上限,是「申请阶段是否失败」的根源;
- 申请完 8G 内存后会不会被使用?只申请不访问,与申请后立刻读写,完全是两种结局;
- 操作系统有没有使用 Swap 机制?这决定了物理内存不足时进程是「被杀」还是「活着」。
下面分场景逐一讨论。
前置知识:malloc 申请的是虚拟内存,而不是物理内存
应用程序通过malloc()函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存,这时会发现这个虚拟内存没有映射到物理内存,CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler(缺页中断处理函数)处理。
缺页中断处理函数会检查是否有空闲的物理内存:
- 如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系;
- 如果没有空闲的物理内存,内核就会开始进行回收内存的工作;如果回收工作结束后,空闲物理内存仍然无法满足此次申请,内核就会放出最后的大招——触发OOM(Out of Memory)机制。
关于 malloc 更底层的实现(brk/mmap 两条分配路径、128KB 阈值、malloc(1) 实际预分配 132KB、free 是否归还操作系统等),可进一步阅读本仓库的 malloc 是如何分配内存的? 一文。
32 位与 64 位系统的虚拟地址空间大小
32 位操作系统和 64 位操作系统的虚拟地址空间大小是不同的。在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分:
- 32 位系统:内核空间占用
1G,位于最高处,剩下的3G是用户空间; - 64 位系统:内核空间和用户空间都是
128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。
为什么虚拟地址空间要这样划分、进程的虚拟地址如何通过 MMU 与页表映射到物理地址、为什么会有分段、分页、多级页表与 TLB 等机制,可阅读本仓库的 为什么要有虚拟内存? 一文作为理论基础。
32 位系统场景:申请 8GB 内存,直接申请失败
现在可以回答这个问题了:在 32 位操作系统、4GB 物理内存的机器上,申请 8GB 内存,会怎么样?
因为 32 位操作系统下,进程最多只能申请 3 GB 大小的虚拟内存空间(用户空间上限),所以进程申请 8GB 内存的话,在申请虚拟内存的阶段就会失败——连虚拟内存都申请不到,根本轮不到物理内存出场。失败的错误大概率是Cannot allocate memory(无法申请内存失败)。
64 位系统场景:申请 8G 虚拟内存没有任何问题
在 64 位操作系统、4GB 物理内存的机器上,申请 8G 内存,会怎么样?
64 位操作系统下,进程可以使用128 TB 大小的虚拟内存空间,所以进程申请 8GB 内存是没问题的——因为进程申请内存是申请虚拟内存,只要不读写这个虚拟内存,操作系统就不会分配物理内存。
我们可以做一个简单的实验验证。作者的服务器是 64 位操作系统,但物理内存只有 2 GB。现在在这台机器上连续申请 4 次 1 GB 内存,也就是一共申请 4 GB 内存。注意下面的代码只是单纯分配了虚拟内存,并没有使用该虚拟内存:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MEM_SIZE 1024 * 1024 * 1024 int main() { char* addr[4]; int i = 0; for(i = 0; i < 4; ++i) { addr[i] = (char*) malloc(MEM_SIZE); if(!addr[i]) { printf("执行 malloc 失败,错误:%s\n",strerror(errno)); return -1; } printf("主线程调用 malloc 后,申请 1gb 大小得内存,此内存起始地址:0X%p\n", addr[i]); } //输入任意字符后,才结束 getchar(); return 0; }运行这段代码,可以看到:虽然物理内存只有 2GB,但程序正常分配了 4GB 大小的虚拟内存——这正印证了「malloc 申请的是虚拟内存,不访问就不占用物理内存」的结论。
通过下面这条命令可以查看进程(test)的虚拟内存大小:
# ps aux | grep test USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 7797 0.0 0.0 4198540 352 pts/1 S+ 16:58 0:00 ./test其中,VSZ 代表进程使用的虚拟内存大小,RSS 代表进程使用的物理内存大小。可以看到,VSZ 大小为 4198540,也就是约 4GB 的虚拟内存,而 RSS 只有 352KB——物理内存几乎没有被占用。
为什么有的读者申请 4GB 虚拟内存会失败?
有读者反馈,说自己做了同样的实验,却发现在 64 位操作系统上申请 4GB 虚拟内存时失败了,错误提示正是Cannot allocate memory。这是为什么呢?
排查后发现,这与 Linux 中的overcommit_memory参数有关。可以使用下面的命令查看这个参数:
cat /proc/sys/vm/overcommit_memory该参数接受三个值:
| 值 | 含义 | 说明 |
|---|---|---|
0(默认值) | Heuristic overcommit handling | 允许 overcommit,但过于明目张胆的 overcommit 会被拒绝。比如 malloc 一次性申请的内存大小就超过了系统总内存。Heuristic 的意思是「试探式的」,内核利用某种算法猜测内存申请是否合理,大概可以理解为单次申请不能超过 free memory + free swap + pagecache 的大小 + SLAB 中可回收的部分,超过就会拒绝 overcommit |
1 | Always overcommit | 允许 overcommit,对内存申请来者不拒 |
2 | Don't overcommit | 禁止 overcommit |
当时那位读者的overcommit_memory参数是默认值 0,所以申请失败的原因可能是内核认为申请的内存太大了、不合理,于是malloc()返回了Cannot allocate memory错误。申请 4GB 虚拟内存失败的同学,可以将overcommit_memory设置为 1:
echo 1 > /proc/sys/vm/overcommit_memory设置完为 1 后,那台机器就可以正常申请 4GB 虚拟内存了。
需要说明的是:作者自己的环境overcommit_memory也是 0,在 64 位系统、2G 物理内存场景下同样成功申请了 4G 内存。怀疑不同版本的内核在overcommit_memory为 0 时,检测内存申请是否合理的算法可能不同。总之,如果申请大内存时不想被内核「检测内存申请是否合理」的算法干扰,将overcommit_memory设置为 1 即可。
那么将
overcommit_memory设置为 1 之后,64 位的主机就可以申请接近 128T 虚拟内存了吗?
不一定,还得看服务器的物理内存大小。读者的服务器物理内存是 2 GB,实验后发现,进程还没申请到 128T 虚拟内存就被杀死了。注意,这次是killed,而不是Cannot Allocate Memory——说明并不是内存申请有问题,而是触发了 OOM。
为什么会触发 OOM?因为即使 malloc 申请的是虚拟内存、不访问就不映射物理内存,申请虚拟内存的过程中还是会使用到物理内存(比如内核保存虚拟内存的数据结构也占用物理内存)。如果主机只有 2GB 物理内存,大概率会触发 OOM。
可以使用top命令,连续按两下 m 键,通过进度条观察物理内存使用情况:可以看到申请虚拟内存的过程中,物理内存使用量一直在增长。直到直接内存回收之后,也无法回收出一块空间供这个进程使用,这时就会触发 OOM,给所有能杀死的进程打分,分数越高的进程越容易被杀死。在这里当然是这个进程得分最高,操作系统就将它杀死,所以最后出现的是 killed,而不是Cannot allocate memory。
那么 2GB 物理内存的 64 位操作系统,就不能申请 128T 的虚拟内存了吗?
其实可以,上面的情况是还没开启 Swap 的情况。使用swapfile 方式开启 1GB 的 Swap 空间之后再实验,发现虽然出现了Cannot allocate memory,但到这一步其实已经成功了——计算一下,已经申请了127.998T 虚拟内存。
实际上,我们是不可能申请完整个 128T 的用户空间的,因为程序运行本身也需要申请虚拟空间。再试试申请 127T 虚拟内存:进程既没有被杀死,也没有出现Cannot allocate memory,正好是 127T 虚拟内存空间。在top中可以看到这个申请了 127T 虚拟内存的进程。
Swap 机制的作用:物理内存不够时,它是最后的缓冲
前面讨论了 32 位/64 位操作系统环境下,申请的虚拟内存超过物理内存后会怎么样:
- 在32 位操作系统,因为进程最大只能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败;
- 在64 位操作系统,因为进程最大只能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也没问题,因为申请的是虚拟内存。
程序申请的虚拟内存,如果没有被使用,它是不会占用物理空间的。当访问这块虚拟内存后,操作系统才会进行物理内存分配。如果申请的物理内存大小超过了空闲物理内存大小,就要看操作系统有没有开启 Swap 机制:
- 没有开启 Swap 机制:程序会直接 OOM;
- 有开启 Swap 机制:程序可以正常运行。
什么是 Swap?
当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间会被临时保存到磁盘,等到那些程序要运行时,再从磁盘中恢复保存的数据到内存中。
另外,当内存使用存在压力的时候,会开始触发内存回收行为,会把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
这种将内存数据换出磁盘、又从磁盘恢复数据到内存的过程,就是Swap 机制负责的。Swap 就是把一块磁盘空间或者本地文件当成内存来使用,它包含换出和换入两个过程:
- 换出(Swap Out):把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存;
- 换入(Swap In):在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。
使用 Swap 机制的优点是,应用程序实际可以使用的内存空间将远远超过系统的物理内存。由于硬盘空间的价格远比内存低,这种方式无疑是经济实惠的。当然,频繁地读写硬盘会显著降低操作系统的运行速率,这也是 Swap 的弊端。
Swap 在什么场景下触发?
Linux 中的 Swap 机制会在内存不足和内存闲置两种场景下触发:
- 内存不足:当系统需要的内存超过了可用的物理内存时,内核会将内存中不常使用的内存页交换到磁盘上,为当前进程让出内存,保证正在执行的进程的可用性。这个内存回收的过程是强制的直接内存回收(Direct Page Reclaim)。直接内存回收是同步的过程,会阻塞当前申请内存的进程;
- 内存闲置:应用程序在启动阶段使用的大量内存在启动后往往都不会再使用,通过后台运行的守护进程kSwapd,可以将这部分只使用一次的内存交换到磁盘上,为其他内存申请预留空间。kSwapd 是 Linux 负责页面置换(Page replacement)的守护进程,它会在空闲内存低于一定水位时回收内存页中的空闲内存,保证系统中其他进程可以尽快获得申请的内存。kSwapd 是后台进程,所以回收内存的过程是异步的,不会阻塞当前申请内存的进程。
关于内存回收更完整的机制——文件页与匿名页的回收差异、LRU 算法、swappiness、min_free_kbytes、NUMA 下的zone_reclaim_mode,以及 OOM Killer 的oom_badness()打分算法,可阅读本仓库的 内存满了,会发生什么? 一文。
如何启用 Swap?
Linux 提供了两种不同的方法启用 Swap:
- Swap 分区(Swap Partition):硬盘上的独立区域,该区域只会用于交换分区,其他文件不能存储在该区域上。可以使用
swapon -s命令查看当前系统上的交换分区; - Swap 文件(Swapfile):文件系统中的特殊文件,它与文件系统中的其他文件没有太多区别。
Swap 换入换出的是什么类型的内存?
内核缓存的文件数据,因为都有对应的磁盘文件,所以在回收文件数据的时候,直接写回到对应的文件就可以了。
但是像进程的堆、栈数据等,它们没有实际载体,这部分内存被称为匿名页。而且这部分内存很可能还要再次被访问,所以不能直接释放内存,于是就需要有一个能保存匿名页的磁盘载体,这个载体就是 Swap 分区。
匿名页回收的方式就是通过 Linux 的 Swap 机制:Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用;再次访问这些内存时,重新从磁盘读入内存就可以了。
接下来,通过两个实验,看看申请的物理内存超过物理内存会怎样:
- 实验一:没有开启 Swap 机制;
- 实验二:有开启 Swap 机制。
实验一:没有开启 Swap 机制,进程被 OOM 杀死
作者的服务器是 64 位操作系统,物理内存只有 2 GB,而且没有 Swap 分区。改造前面的代码,在申请完 4GB 虚拟内存后,通过memset函数实际访问这块虚拟内存,看看在没有 Swap 分区的情况下会发生什么:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MEM_SIZE 1024 * 1024 * 1024 int main() { char* addr[4]; int i = 0; for(i = 0; i < 4; ++i) { addr[i] = (char*) malloc(MEM_SIZE); if(!addr[i]) { printf("执行 malloc 失败,错误:%s\n",strerror(errno)); return -1; } printf("主线程调用 malloc 后,申请 1gb 大小得内存,此内存起始地址:0X%p\n", addr[i]); } for(i = 0; i < 4; ++i) { printf("开始访问第 %d 块虚拟内存 (每一块虚拟内存为 1 GB)\n", i + 1); memset(addr[i], 0, MEM_SIZE); } //输入任意字符后,才结束 getchar(); return 0; }运行结果:在访问第 2 块虚拟内存的时候(每一块 1 GB,此时已访问总量超过机器的物理内存 2GB),进程(test)被操作系统杀掉了。
通过查看message系统日志,可以发现该进程是被操作系统的OOM Killer 机制杀掉的,日志里报错了Out of memory,也就是发生了 OOM(内存溢出错误)。
什么是 OOM?
内存溢出(Out Of Memory,简称 OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出。
实验二:有开启 Swap 机制,32GB 内存正常使用
作者用 MacBook Pro 笔记本做测试:64 位操作系统,物理内存 8 GB,Swap 分区大小为 1 GB。
注意:macOS 的 Swap 分区总大小是动态变化的——当没有使用 Swap 分区时,总大小是 0;当使用了 Swap 分区,总大小会增加至 1 GB;当已使用大小超过 1 GB 时,总大小增加到 2 GB;超过 2 GB 时增加到 3GB,如此往复。这是 macOS 自己的实现;Linux 的 Swap 分区则是固定大小的,不会根据使用情况自动增长。
为了方便观察磁盘 I/O 情况,改进一下前面的代码:分配完 32 GB 虚拟内存后(笔记本物理内存是 8 GB),通过一个 while 循环频繁访问虚拟内存:
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MEM_SIZE 32 * 1024 * 1024 * 1024 int main() { char* addr = (char*) malloc((long)MEM_SIZE); printf("主线程调用 malloc 后,目前共申请了 32gb 的虚拟内存\n"); //循环频繁访问虚拟内存 while(1) { printf("开始访问 32gb 大小的虚拟内存...\n"); memset(addr, 0, (long)MEM_SIZE); } return 0; }运行结果:在有 Swap 分区的情况下,即使笔记本物理内存只有 8 GB,申请并使用 32 GB 内存也没问题,程序正常运行,没有发生 OOM。
从top可以看到:进程的内存显示 32 GB(这里不要理解为占用的物理内存,应理解为已被访问过的虚拟内存大小,也就是在物理内存「呆过」的内存大小),系统已使用的 Swap 分区达到2.3 GB。
此时笔记本的磁盘开始出现「沙沙」的声音,查看磁盘 I/O 情况,可以看到磁盘 I/O 达到了一个很高的峰值——这就是 Swap 频繁换入换出带来的代价:内存数据被换出到磁盘、再次访问时又从磁盘读入,频繁的磁盘读写显著拖慢了系统。
有了 Swap 分区,进程可以使用无上限的内存吗?
当然不是。把上面的代码改成申请 64GB 内存后,当进程用到56 GB(已被访问的虚拟内存大小)的时候,进程就被系统 kill 掉了。
当系统多次尝试回收内存,还是无法满足所需使用的内存大小时,进程就会被系统 kill 掉,意味着发生了 OOM。
总结:不同场景下的最终答案
至此,验证完成。简单总结一下:
- 在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败;
- 在 64 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
- 没有 Swap 分区:因为物理空间不够,进程会被操作系统杀掉,原因是OOM(内存溢出);
- 有 Swap 分区:即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行。
面试作答速记
围绕这道面试题,可以按以下三层递进组织答案,每一层都有对应的验证手段:
- 申请阶段:malloc 申请的是虚拟内存,能否申请成功取决于地址空间上限与
overcommit_memory策略。32 位下 3GB 上限直接判死;64 位下用cat /proc/sys/vm/overcommit_memory判断策略,值 0 时单次超大申请可能被拒,值 1 时来者不拒,值 2 时禁止 overcommit; - 访问阶段:虚拟内存被读写时通过缺页中断按需映射物理内存,可用
ps aux中的 VSZ(虚拟内存)与 RSS(物理内存)对照观察,也可用top按两次 m 看物理内存进度条增长; - 物理内存耗尽阶段:先内存回收(详见内存回收一文),回收无果则触发 OOM Killer 按
oom_badness()打分杀进程;开启 Swap 后,进程可借助磁盘临时存储超过物理内存的匿名页,代价是磁盘 I/O 飙升、系统变卡。
延伸阅读
- 为什么要有虚拟内存?:虚拟地址与物理地址映射、分段、分页、多级页表、TLB、Linux 虚拟地址空间布局;
- malloc 是如何分配内存的?:brk/mmap 两条分配路径、malloc(1) 预分配 132KB、free 的释放语义;
- 内存满了,会发生什么?:文件页/匿名页回收、kswapd 水位、swappiness、OOM Killer 打分与
oom_score_adj保护进程的方法; - 图解系统介绍:操作系统进程管理、内存管理、文件系统、设备管理、网络系统五大结构的完整知识目录。
- 文档
- 教程
- 知识库
【免费下载链接】CS-Base
图解计算机网络、操作系统、计算机组成、数据库,共 1000 张图 + 50 万字,破除晦涩难懂的计算机基础知识,让天下没有难懂的八股文!🚀 在线阅读:https://xiaolincoding.com
相关推荐
Compound Engineering 终极指南:33 个 AI 技能如何让开发越来越快
Compound Engineering 终极指南:33 个 AI 技能如何让开发越来越快 Compound Engineering 是一款面向 Claude
人工智能AI 技能AI 插件VidBee 实测:3 步搞定 1000+ 网站视频下载
VidBee 实测:3 步搞定 1000+ 网站视频下载 想把一节两小时的在线教程存到平板上离线看,网页上却只有分享按钮;视频站要求登录,抓到的流地址几分钟就失
桌面应用音视频AI 应用语音CS-Base 图解系统:为什么要有虚拟内存?从分段分页到多级页表与 TLB 的完整内存管理图解
CS Base 图解系统:为什么要有虚拟内存?从分段分页到多级页表与 TLB 的完整内存管理图解 本篇技术指南以 CS Base 仓库《图解系统》中 为什么要有
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考