一、虚拟内存
虚拟内存是一种实现在计算机软硬件之间的内存管理技术,它将程序使用到的内存地址(虚拟地址)映射到计算机内存中的物理地址,虚拟内存使得应用程序从繁琐的内存空间管理中解放出来,通过内存隔离提高了内存安全性。
虚拟内存地址通常是连续的地址空间,由操作系统的内存管理模块控制,在触发缺页中断时利用分页技术将实际的物理内存分配给虚拟内存。同时虚拟内存的空间大小远超出实际物理内存的大小,虚拟内存技术使得进程可以使用比物理内存大得多的内存空间。
二、C 中的虚拟内存布局
C 语言中一个进程的内存映像从低地址开始分为正文段、初始化数据段、未初始化数据段、堆区、栈区五大部分:
- Text 段:Text 段是指用来存放程序执行代码的一块内存区域,是二进制文件(或者说处理器的机器指令)在内存中的映像。当然也有可能包含一些只读的常数变量,例如字符串常量等。这部分区域的大小在程序运行前就已经确定,并且通常只允许进行读操作,向 Text段 写会导致 Segmention Fault。
- Data 段:Data 段是指存放程序中已经初始化的全局变量和静态变量的一块内存区域。Data 段并不是匿名的,而是映射了程序二进制文件中在编译时就已初始化的数据,由程序初始化。
- BSS 段:BSS 段存放了未初始化的全局变量和静态变量,由操作系统初始化(清零),且是匿名的不映射任何文件,不占用外存空间,只在运行时占用内存。注意,这里的未初始化指的是没有显示初始化,因为全局变量和静态变量会自动隐式初始化为0,但我们没有必要把这些 0 都存储起来,从而节省外存空间,这也是 BSS段的主要作用。
- 堆区:堆提供了程序运行时的内存分配,堆内存的生命周期在函数之外,大部分语言都提供了堆内存管理函数,如 C 语言的
malloc()
与free()
,因此堆区由用户管理,可控性强。堆内存分配的算法非常复杂,既要保证内存分配的实时和快速,又要尽量避免堆中出现过多碎片,由此也就引申出了 FF、BF、WF、NF 一系列内存分配算法与分配策略。如果当前堆的内存足够程序使用,则不需要与内核交互,在当前堆中寻找可用内存就行,否则的话需要调用brk()
系统调用向内核申请空间。 - 栈区:栈保存了局部变量、函数形参、返回地址等,调用一个新的函数会在栈上创建一个新的栈帧,当函数返回时这个栈帧会被自动销毁。一个栈帧包括:函数的返回地址和参数、临时变量(包括函数的非静态局部变量以及编译器自动生成的其他临时变量)、栈帧状态值( EBP 和 ESP,划定了这个函数的栈帧的范围)。栈的空间分配由指令集中专用的机器指令实现,当栈空间用尽后继续push会触发栈空间的扩展,导致 Page Fault,然后在内核中调用
expand_stack()
,该函数调用acct_stack_growth()
来判断是否可以增长栈空间,如果当前栈空间的大小小于RLIMIT_STACK
,则可以继续增长栈空间,该过程由内核完成进程不会感知到。当用户的栈空间已经达到允许的最大值时,内核会给进程发送一个 Segmentation Fault 信号终止该进程,因此进程的栈空间只会增大不会缩小。
三、C++ 中的虚拟内存布局
C++ 中的虚拟内存分配与 C 中大体上相似,只在细节处略有不同:
- 代码区(.text):存放只读的程序二进制代码(机器指令),由操作系统进行管理。
- 常量区(.rodata):常量区是一块比较特殊的存储区,专门用来存储那些由 const 修饰的变量以及常量字符串等不能被修改的常量,在程序结束后由系统释放。
- 全局/静态存储区(.bss 和 .data):存放全局变量和静态变量,程序一经编译该区域就会存在。在C++ 中,由于编译器会对全局变量和静态变量进行自动初始化并赋值,所以并没有像 C 中一样对初始化变量和未初始化变量进行区分。全局/静态存储区会在程序结束后由系统释放。
- 堆区和自由存储区:堆是通过
malloc
、calloc
或realloc
分配的内存,并需要通过free
释放。如果程序员没有进行free
操作,则会造成内存泄露,并在程序结束时由 OS 进行内存回收。自由存储区是对由new
申请,并由delete
或delete[]
释放的堆区的一个抽象。 - 栈区:保存函数中的局部变量、函数参数以及返回地址。由编译器负责分配释放,函数结束栈变量也随之失效。
四、堆区和栈区的区别
堆区 | 栈区 | |
---|---|---|
管理者 | 由用户管理,可控性强 | 由系统管理,分配效率高 |
地址扩展 | 从低地址向高地址扩展 | 从高地址向低地址扩展 |
分配方式 | 堆是 C/C++ 函数库提供的数据结构,库函数会按照一定的算法基于空闲链表在堆内存中搜索可用的足够大小的空间,分配速度慢,而且会产生内存碎片 | 栈是操作系统提供的数据结构,出栈入栈由专用的机器指令实现,分配速度快,不存在内存碎片 |
存储内容 | 由用户决定运行时的内存分配 | 栈保存了局部变量、函数形参、返回地址等 |
生命周期 | 堆内存的生命周期在函数之外,由用户控制 | 调用一个新的函数会在栈上创建一个新的栈帧,当函数返回时这个栈帧会被自动销毁 |