目录
一、malloc的内存分配
1、malloc是如何分配内存的 ?
咱们都知道操作系统将内存分为,栈区、堆区、全局静态区、代码区。(虽然不同操作系统细分的区域有所不同,但是大致都会包含这四个区)。malloc申请不是真正的在物理内存上申请内存,它也是在虚拟内存的中申请。实际上malloc()函数 并不是一个系统调用,而是 C 库里的函数,它用于动态分配内存。当我们使用malloc 申请内存的时候,malloc底层会有两种方式向操作系统申请堆内存。malloc其原型void *malloc(unsigned int num_bytes);
- 方式一:通过 brk() 系统调用从堆分配内存
- 方式二:通过 mmap() 系统调用在文件映射区域分配内存;
方式一:这种方式就是通过调用brk()函数,将堆顶的指针想高地址移动所需大小个。
方式二:通过 mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。
什么时候使用brk什么时候使用mmap呢?
- 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
- 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
注意:不同的 glibc (c运行库)版本可能阈值不同。
2、malloc申请空间一定就是我们指定的空间大小吗?
实际上我们使用malloc申请指定大小的空间时,malloc并不是只申请指定空间的大小,它会申请比指定空间大一点的空间。《UNIX环境高级编程》一书中写过这样一段话:“应当注意的是,大多数实现所分配的存储空间比所要求的要稍微大一些,额外的空间用来记录管理信息——分配块长度,指向下一个分配块的指针等等。这就意味着写过一个已分配区的尾端,将改写后一块的管理信息,着是灾难性的。”
调用malloc函数,它会返回一个指针,这个指针指向申请空间的起始位置。另外会多申请一块空间,来保存我们申请这片空间的信息,比如我们申请空间的长度是多少,起始地址。 多申请这片空间的目的是为了方便free的使用。
不同操作系统中”head“的数据结构也是不一样的,一般是一个联合体,内部嵌套一个结构体,结构体中包含两个成员,一个是指向下一个头部的指针,一个表示堆内存的大小。
除了多申请一小块空间来保存信息外,因为有内存管理机制,所有当我们申请空间的时候,内存管理器并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。
总结:
- 申请内存大于128时或者某一个阈值时,调用mmap系统调用,从文件映射区偷一块。小于阈值时调用brk移动堆顶指针。
- malloc申请空间比实际大的原因:一是:需要一小块空间保存申请的信息。二是:内存管理器会预分配更大空间做为内存池。
3、为什么不只使用mmap来申请空间?
向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后在回到用户态,而用户态到内核态的切换是会耗费不少时间的,所以,申请内存的操作应该避免频繁的系统调用,如果都用 mmap 来分配内存,等于每次都要执行系统调用。(mmap就是一个系统调用嘛)。
mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。因为归还给操作系统了虚拟内存和物理内存的映射关系都不在了,而brk申请然后释放是将这块内存块重新标记为可用,映射关系还在。
也就是说频繁通过 mmap 分配的内存,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
而brk方式可以改进mmap带来的这两个问题。
4、为什么不只使用brk来申请空间?
我们知道brk()申请的空间,是将堆顶指针往高地址移动,而释放的时候它不会归还给操作系统,而是将该内存块标记为可用,然后放入内存池。
我们考虑这样一个场景:如果我们申请了10kb、20kb、30kb的内存空间,然后我们将它释放掉,如果下一次我们申请的空间是30kb或者比20、10kb小,我们都可用重用内存池的空间,如果申请的是50kb,那么我们不得不重新向OS申请,导致时间内存逐渐增大。
所有使用brk()申请空间,特别是在数据大小不一零碎的时候,特别容易造成堆内存产生越来越多的不可用的小碎片,而导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。(虽然内存管理器可能会重组小内存,但是也是需要消耗一定资源的)
所有只使用brk也不是非常完美的选择,而应该是mmap和brk协调使用。
二:free是如何释放空间的。
1、freer是如何知道需要释放的空间大小的?
我们在写C++程序的时候直接就是传入了指针。
int* ptr = malloc int(50);
free(ptr);
并没有告诉free需要释放空间的大小是多少啊?它是怎么知道的呢?(大疆问过)
前面咱们说malloc申请空间时候会多申请一块空间“head”,这里面就保存了我们申请这片空间的大小,所以free传入需要释放空间的首地址,通过首地址向前偏移操作系统就能读到"head"保存的信息,操作系统就能知道释放空间是多大了。
2、free 释放内存,会归还给操作系统吗?
free释放内存后是否会归回给操作系统,也是取决于malloc申请时具体调用的是哪一种方法申请的。
- malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用。
- malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。
注意:如果内存还在,你释放了去打印它的地址它会出现一个具体的地址,比如2F530010。所以释放后一般要把ptr = nullptr。如果内存真正的释放了,你去打印是不会出现比如2F530010,这样的地址的。
三:malloc
/realloc
/calloc
的区别
malloc前面说了,这里主要说realloc和calloc。首先三者都是动态分配内存,都是stdlib.h库里的函数,但是也存在一些差异。
- malloc函数其原型:void *malloc(unsigned int num_bytes)。num_byte为要申请的空间大小,需要我们手动的去计算,如int *p = (int *)malloc(20*sizeof(int))。申请后空间的值随机,不会初始化。
- calloc函数其原型:void *calloc(size_t n, size_t size)。其比malloc函数多一个参数,并不需要人为的计算空间的大小,比如要申请20个int类型空间,会int *p = (int *)calloc(20, sizeof(int)),不需要人为计算。calloc在申请后,对空间逐一进行初始化,并设置值为0。
- realloc函数和上面两个有本质的区别,其原型void realloc(void *ptr, size_t new_Size)。
用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作),ptr为指向原来空间基址的指针, new_size为接下来需要扩充容量的大小。总结:
malloc:一个参数,需要手动计算空间大小,不会初始化,空间的值随机。
calloc:两个参数,不需要手动计算,会初始化为0。
realloc:是用来扩容的。
问:为什么不直接使用calloc?
答:calloc虽然很方便,不需要我们手动计算申请空间的大小,但是你人不计算,计算机需要计算,而且calloc还会一一初始化为0,这导致了它的效率没有malloc快,在某些不需要初始化的场合没有malloc好。
1、realloc是如何扩容的?
realloc是在申请空间不够的情况下进行扩容,我们都知道申请空间时,内存空间管理器会预申请比需要空间大的一块区域,以便后面更快的申请。
所以当我们申请的空间size:
- 比预留空间小:(即比原空间后面的预申请空间小)直接在预申请的空间中开辟一块,返回原空间基地址。
- 比预留空间大:系统将重新申请一片空间,将原空间数据拷贝过去,返回新空间地址。
- 申请size非常大:申请失败,realloc返回空指针,原来空间不会被释放。
- 扩容后的内存空间较原空间小:将会出现数据丢失,如果直接realloc(p, 0);相当于free(p)。
四:new与delete
在C++中有我们自定义的数据类型,比如类的对象,这个使用malloc和free就行不通了,于是就有了new和delete,new和malloc没有本质上的区别,都是在堆区申请内存,只不过new会调用构造函数,delete会调用析构函数。而且new和delete不是C库函数,而是一个操作符。
1、new和delete的底层实现
在面向过程的语言中(C语言)出现错误或者异常通常是返回一个值。而在面向对象的语言中出现错误通常是抛出异常。而free和delete一般不会出错,出错一般是释放空间的首地址不对,或者重复释放。
在使用new时
实际上new底层调用了两个函数,operator new
和构造函数。operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
其中operator new 是对malloc的封装,使得底层调用malloc出错返回0时抛出异常。所有new申请空间底层也是调用了malloc函数,只不过还需要调用自定义类型的构造函数。
其中operator delete是对free的封装,delete比free多了调用析构函数。
2、new[]和delete[]的过程
对于new[]的原理如下:
- 调用operator new[]函数,实际在operator new[]中调用operator new函数完成N个对象空间的申请。
- 调用N次构造函数
对于delete[]的原理如下:
- 调用N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放N个对象空间
3、new[]申请的空间可用使用delete释放吗
关于这个问题我查阅了网上所有的资料没有一篇博客能够完整解释,但是我通过查看源码和自己的实验,以及一些文章的启发算是弄明白了它的原因。
首先new[] 申请数组空间时我们一般会指定我们申请的数组空间是多大,比如:
A* a = new A[5];
我们指定了数组的大小为5,这些信息会在申请空间的时候保存在我们内存块的“head”中,如果正常使用delete[]释放空间,它会根据“head”的信息,知道需要释放的自定义数据类型的个数为5,然后调用5次operator delete和5次析构函数来释放它。
但是我们使用delete来释放时就不一样了。
如果我们 new[] 申请的数组类型,是内置数据类型,即int、char、double等。和是自定义类型(类对象),delete产生的效果不一样。
delete和delete[]释放内存的过程:
- 第一步:先调用析构函数(如果是自定义数据类型)。
- 第二步:调用free释放,释放的是这块内存块的空间。
二者的区别就是delete只调用一次,delete[]调用多次析构函数。
结论:
- 自定义类型:(因为类中也可能使用new去申请空间),如果使用delete去释放,仅仅只是数组第一个元素调用了析构函数,其他元素没有调用析构函数。所有只有第一个元素正确得到了释放,如果其他元素对象的内部也使用了new,那就会有很严重的后果C++并没有定义这种情况的处理机制。
- 内置数据类型:因为内置数据类型释放不需要调用析构函数,所以使用delete也是可以释放成功的。
深入理解,看图:
举个例子:我们自定义的类型,比如类对象,我们无法保障这个对象里面不去调用new申请空间,而我们知道类对象释放掉new申请的空间是要在析构函数里面写delete。而你现在使用new[]申请了类对象的数组,如果单纯使用delete释放,那么只有第一个元素调用了析构函数(其余的元素没有没有调用析构函数,意味着其余元素,如果内部使用了new,那么这个空间不能被释放)之后调用free。free释放的只是一个外壳,看上去对,没错调用free(ptr)后,free释放掉了 “ptr+内存块大小” 的这个空间,实际上每一个元素内部调用的new空间你没释放啊,所有free只是释放你new[]返回的ptr自身的这个空间,但是这个空间内部可能还new了。这么解释能听懂吗?理解了这个,不调用析构函数的free只是释放自身这个外壳。所有这也就是为什么,内置数据类型使用delete可以正确释放的原因,以为内置数据类型,本来也不用调用析构函数,所以你free释放本身ptr所指向的这个空间,也就是释放了内置数据类型的这片空间。
五:malloc/free与new/delete的区别总结
1、自由存储区:
new分配内存的位置在自由存储区,自由存储区(可堆可栈区) 是C++中动态分配和释放对象的一个概念,通过new分配的内存区域可以称为自由存储区,通过delete释放归还内存。自由存储区可以是堆、栈等,具体是在哪个区,主要还是要看new的实现以及C++编译器默认new申请的内存是在哪里。但是基本上,很多C++编译器默认使用堆来实现自由存储,运算符new和delete内部默认是使用malloc和free的方式来被实现,说它在堆上也对,说它在自由存储区上也正确。因为在C++中new和delete符号是可以重载的,我们可以重新实现new的实现代码,可以让其分配的内存位置在栈区等。而malloc和free是C里的库函数,无法对其进行重载。
2、返回值类型:
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。所以在C++程序中使用new会比malloc安全可靠。