五大内存分区
在C++中,内存分成5个区,它们分别是:栈、堆、自由存储区、全局/静态存储区和常量存储区。
栈:由编译器自动分配和释放,存放函数的参数值、局部变量的值等。操作方式类似于数据结构中的栈。
堆:堆由程序员手动分配和释放,且完全不同于数据结构中的堆,分配方式类似链表。由new/delete 申请和释放。若程序员忘记释放则由系统于程序结束时回收。
自由存储区:是由malloc等分配的内存块,和堆十分相似,不过是用free来释放。
全局/静态存储区:存放全局变量和静态变量。在C中,全局变量又分为初始化(DATA段)的和未初始化(BSS段)的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域;C++中没有这一区分。
常量存储区:这是一块特殊存储区,里面存放常量,不允许修改。
BSS段、DATA段、CODE段
BSS段(bss segment):通常是用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Startd by Symbol的简称。BSS段属于静态内存分配。
DATA段(data segment):通常是用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
CODE段(code segment):通常是用来存放程序执行代码的一块内存区域,这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的函数变量,例如字符串常量等。
Linux环境程序典型的内存布局如下图所示:
代码段(Text Segment):用户存放CPU执行的机器指令,为防止指令被其它程序修改,代码段一般只读不可更改。比如,源码中的字符串常量存储于代码段,不可修改。
初始化数据段(Data Segment):又称为DATA段,用于存储初始化的全局变量和Static变量,段大小在编译时确定,所以内存的分配属于静态内存分配。
未初始化数据段(BSS Segment,Block Started by Symbol):又称为BSS段,通常用来存放程序中未初始化的全局变量和Static,虽未显示初始化,但在程序载入内存执行时,由内核清0,所以未显示初始化则默认为0。BSS段的大小也是在编译时确定,运行时内存的分配属于静态内存分配。
堆(Heap):用于保存程序运行时动态申请的内存空间,由开发人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间,比如使用malloc()或new申请的内存空间。堆的地址空间“向上增加”,即当堆上保存的数据越多,堆的地址就越高。堆的内存分配属于动态分配,一般运行时才知道分配的内存大小,并且堆可分配存活于函数之外的内存,在未显示调用free()或delete释放时,其生命周期为进程的生命周期。
映射段(Memory Mapping Segment):该区域内核将文件内容直接映射到内存。任何应用程序都可以请求该区域。Linux中通过mmap()系统调用,Windows中通过creatFileMapping()/MapViewOfFile()创建。文件I/O时内存映射方便并且高效,所以,它常用来加载动态库,还可以创建一种匿名映射,并不对应于文件,而用于程序数据。在Linux中,如果使用malloc()申请一块过大的内存,C库函数便会创建这种内存映射段,而不是使用堆内存。“过大”的内存指超过M_MMAP_THRESHOLD字节,默认128KB,可以通过mallopt()函数调整。映射段也属于动态分配。
栈(Stack):用于保存函数的局部变量(但不包括static声明的静态变量,静态变量存放在数据段或BSS段)、参数、返回值、函数返回地址以及调用者环境信息(比如寄存器值)等信息,由系统进行内存的管理,在函数完成执行后,系统自行释放栈区内存,不需要用户管理。整个程序的栈区的大小可以由用户自行设定,Windows默认的栈区大小为1M,可通过Visual Studio更改编译参数手动更改栈的大小。64bits的Linux默认栈大小为10MB,可通过命令ulimit -s临时修改。栈是一种“后进先出”(Last In First Out,LIFO)的数据结构,这意味着最后入栈的数据,将会是第一个出栈的数据。对于那些暂时存贮无需长期保存的信息来说,LIFO这种数据结构非常理想。在调用函数后,系统通常会清除栈上保存的信息。栈另外一个重要的特征是,它的地址空间“向下减少”,即当栈上保存的数据越多,栈的地址就越低。
内核空间(Kernel Space),:用于存储操作系统和驱动程序,用户空间用于存储用户的应用程序,二者不能简单地使用指针传递数据。当一个进程执行系统调用而陷入内核空间执行内核代码时,我们称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态),即此时处理器在执行最低特权级(3级)用户代码中。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。
内存段的特点和区别如下。
|段名|存储内容 |分配方式|生长方向|读写特点|运行态|
|—|---|—|---|—|
|代码段|程序指令、字符串常量、虚函数表|静态分配|由低到高|只读|用户态|
|数据段|初始化的全局变量和静态变量|静态分配|由低到高|可读可写|用户态|
|BSS段|未初始化的全局变量和静态变量|静态分配|由低到高|可读可写|用户态|
|堆|动态申请的数据|动态分配|由低到高|可读可写|用户态|
|映射段|动态链接库、共享文件、匿名映射对象|动态分配|由低到高|可读可写|用户态|
|栈|局部变量、函数参数与返回值、函数返回地址、调用者环境信息|静态分配|由高到低|可读可写|用户态|
|内核空间|储操作系统、驱动程序|动态+静态|由低到高+由高到低|不能直接访问|内核态|
关于内核空间,其中包含内核栈和内核的数据段,所以内存地址生长方向既有由低到高(内核数据段),也有由高到低(内核栈)。关于读写的特点,由内核进行读写,用户程序不可直接访问。
以下面的C++代码为例,看一下常见变量所属的内存段。
#include <string.h> int a = 0; // a在数据段,0为文字常量,在代码段 char *p1; // BSS段,系统默认初始化为NULL void main() { int b; //栈 char *p2 = "123456"; //123456在代码段,p2在栈上 static int c =0; //c在数据段 const int d=0; //栈 static const int d; //数据段 p1 = (char*)malloc(10); //分配得的10字节在堆 strcpy(p1,"123456"); //123456放在字符串常量区,编译器可能会将它与p2所指向的"123456"优化成一个地方 }
参考文献