malloc/free
malloc工作机制
malloc函数的实质体现在,它有一个将可 用的内存块连接为一个长长的列表的所谓空闲链表。调用 malloc 函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分 为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连 接表上。调用 free 函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空 闲链上可能没有可以满足用户要求的片段了。于是, malloc 函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲 块合并成较大的内存块。
分配内存,在size的基础上加sizeof(struct mem_control_block) 个字节。我们主要使用连接的指针遍历内存来寻找开放的内存块。释放内存,将参数指针回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。
C语言是跨平台的,最终的内存处理都是交给系统API完成。系统会记录每一块分配内存的地址,大小,释放情况等等。所以malloc只需传内存大小参数,free只需要传一个地址的参数就可以了。而且同一个地址不能释放两次。
debug
debug版本下malloc需要分配的内存会比实际的size多36byte。最终分配的内存块如下:
_CrtMemBlockHeader是一个双向链表结构,其定义如下:
typedef struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader *pBlockHeaderNext; //下次分配的内存块
struct _CrtMemBlockHeader *pBlockHeaderPrev; //上次分配的内存块
char *szFileName; //分配内存代码的文件名
int nLine; //分配内存代码的行号
size_tnDataSize; //请求的大小,如实例中的
int nBlockUse; //请求的内存类型,如实例中的user类型
long lRequest; //请求id,每次请求都会被记录
unsigned char gap[nNoMansLandSize]; //4字节校验位
} _CrtMemBlockHeader;
用户请求内存前后分别有4字节的校验位,分配内存后都会被初始化为0xFD。如果这8个字节被改写,free时就会触发断言失败。而请求的32字节会被初始化为0xCC(和栈的初始化一样)。
系统通过记录这些信息就能显示的给出错误。比如越界访问请求的内存在debug下会断言失败,release下面则不会,从而这会给程序埋下巨大的隐患。很多在release下偶发的错误就是这样产生的。_CrtMemBlockHeader总共32字节,加上用户请求的32字节及最后4字节校验位是68字节。最终调用系统的API请求内存。比如Windows下面是HeapAlloc。
如果内存分配失败,malloc不像new那样可以调用new_handler来处理,它直接返回NULL。
free则是对_CrtMemBlockHeader的信息做清理操作,检查校验位等等。最终调用系统API释放内存。比如Windows下面是HeapFree。
release
实际分配的内存等于请求的内存大小。malloc和free只是在系统API之上做了些判断操作。
内存碎片
内存分配的原理
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
1、brk是将数据段(.data)的最高地址指针_edata往高地址推;
2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。
下面以一个例子来说明内存分配的原理:
情况一:malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:
1.进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。
其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。_edata指针(glibc里面定义)指向数据段的最高地址。
2. 进程调用A=malloc(30K)以后,内存空间如图2:
malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。
你可能会问:只要把_edata+30K就完成内存分配了?
事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
3. 进程调用B=malloc(40K)以后,内存空间如图3。
情况二: malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:
4、进程调用C=malloc(200K)以后,内存空间如图4:
默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。
这样子做主要是因为brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。
当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。
5、进程调用D=malloc(100K)以后,内存空间如图5;
6、进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放。
7、进程调用free(B)以后,如图7所示:
B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?
当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。
8、进程调用free(D)以后,如图8所示:
B和D连接起来,变成一块140K的空闲内存。
9、默认情况下:
当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩
malloc/free和new/delete
malloc 和 new 至少有两个不同 : new 返回指定类型的指针,并且可以自动计算所需要大小。malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。malloc 与 free 是 C++/C 语言的标准库函数, new/delete 是 C++ 的运算符。它们都可 用于申请动态内存和释放内存。 对于非内部数据类型的对象而言,光用maloc/free 无法满足动态对象的要求。对象 在创建的同时要自动执行构造函数, 对象在消亡之前要自动执行析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free 。 因此C++ 语言需要一个能完成动态内存分配和初始化工作的运算符 new ,以及一个 能完成清理与释放内存工作的运算符delete 。注意 new/delete 不是库函数。如果没有足够内存空间而导致malloc申请失败,返回NULL。而new则抛出std::bad_alloc标准异常,不返回NULL。
malloc和new的区别
1)new 返回指定类型的指针,并且可以自动计算所需要大小。而 malloc 则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针。
2)malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。
有了malloc/free为什么还要new/delete?
1) malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2) 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?
这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。