在c++中内存主要分为5个存储区:
栈(Stack):局部变量,函数参数等存储在该区,由编译器自动分配和释放。栈属于计算机系统的数据结构,进栈出栈有相应的计算机指令支持,而且分配专门的寄存器存储栈的地址,效率高,内存空间是连续的,但栈的内存空间有限。
堆(Heap):需要程序员手动分配和释放(new,delete),属于动态分配方式。内存空间几乎没有限制,内存空间不连续,因此会产生内存碎片。操作系统有一个记录空间内存的链表,当收到内存申请时遍历链表,找到第一个空间大于申请空间的堆节点,将该节点分配给程序,并将该节点从链表中删除。一般,系统会在该内存空间的首地址处记录本次分配的内存大小,用于delete释放该内存空间。
全局/静态存储区:全局变量,静态变量分配到该区,到程序结束时自动释放,包括DATA段(全局初始化区)与BSS段(全局未初始化段)。其中,初始化的全局变量和静态变量存放在DATA段,未初始化的全局变量和静态变量存放在BSS段。BSS段特点:在程序执行前BSS段自动清零,所以未初始化的全局变量和静态变量在程序执行前已经成为0。
文字常量区:存放常量,而且不允许修改。程序结束后由系统释放。
程序代码区:存放程序的二进制代码。
首先malloc肯定是从堆中分配内存,而堆又在用户空间中占据什么位置?通过下面这张图可以看出来:
很明显是32位系统,寻址空间是4G,linux系统下0-3G是用户空间,3-4G是内核空间。而在用户空间下又分为代码段、数据段、.bss段、堆、栈。各个segment所含内容在图中有具体说明。
其中,代码段:存放函数和一些常量。bss段:存放未初始化的全局变量和静态变量。数据段:存放已经初始化的全局变量和局部静态变量。堆:动态分配的内存。栈:局部变量。
通过命令 ulimit -s
查看linux的默认栈空间大小,默认情况下为8192 KB,即8MB。也可以临时修改栈空间的默认大小,ulimit -s 102400
,即修改为100MB。
限制栈的大小主要是因为栈的地址空间必须连续,如果任其任意成长,会给内存管理带来困难。
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间可以供进程访问;而从break往上是不能访问的。我们用malloc进行内存分配就是将break指针向高地址移动。
// brk将break指针直接设置为某个地址,而sbrk将break
// 从当前位置移动increment所指定的增量。
int brk(void *addr);
void *sbrk(intptr_t increment);
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
brk的作用是扩展堆地址的上界;
mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
malloc小于128k的内存,使用brk分配内存;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。
ptmalloc将相似大小的内存块用双向链表链接起来,这样的一个链表被称为一个 bin。 Ptmalloc一共维护了128个bin,并使用一个数组来存储这些 bin。
数组中的第一个为 unsorted bin, 数组中从 2 开始编号的前 64 个 bin 称为 small bins,同一个 small bin中的 chunk具有相同的大小。两个相邻的 small bin中的 chunk大小相差 8bytes。
small bins 中的 chunk 按照最近使用顺序进行排列,最后释放的 chunk 被链接到链表的头部,而申请 chunk 是从链表尾部开始,这样,每一个 chunk 都有相同的机会被 ptmalloc 选中。
Small bins 后面的 bin 被称作 large bins。 large bins 中的每一个 bin 分别包含了一个给定范围内的 chunk,其中的 chunk 按大小序排列。相同大小的 chunk 同样按照最近使用顺序排列。
1. malloc函数的实质是它有一个将可用的内存块连接为一个长长的列表即空闲链表。
2. 调用malloc()函数时,它沿着空闲链表寻找一个大到足以满足用户请求所需要的内存块(常见的策略是首次适配和最佳适配)。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到空闲链表上。
3. 调用free()函数时,它将用户释放的内存块连接到空闲链表上。
4. 到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。
#define BLOCK_SIZE 24
void *first_block=NULL;
/* other functions... */
void *malloc(size_tsize){
t_blockb,last;
size_ts;
/* 对齐地址 */
s= align8(size);
if(first_block){
/* 查找合适的block */
last= first_block;
b= find_block(&last,s);
if(b){
/* 如果可以,则分裂 */
if((b->size- s)>= (BLOCK_SIZE +8))
split_block(b,s);
b->free= 0;
}else {
/* 没有合适的block,开辟一个新的 */
b= extend_heap(last,s);
if(!b)
returnNULL;
}
}else {
b= extend_heap(NULL,s);
if(!b)
returnNULL;
first_block= b;
}
returnb->data;
}
【Note】:
首次适配:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块。
最佳适配:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块。
我们可以看出new和delete内部的调用顺序:(new和delete)
我们都知道,使用 malloc/calloc 等分配内存的函数时,一定要检查其返回值是否为“空指针”(亦即检查分配内存的操作是否成功),这是良好的编程习惯,也是编写可靠程序所必需的。但是,如果你简单地把这一招应用到 new 上,那可就不一定正确了。我经常看到类似这样的代码:
int* p = new int[SIZE];
if ( p == 0 ) // 检查 p 是否空指针
return -1;
// 其它代码
其实,这里的 if ( p == 0 ) 完全是没啥意义的。在C++里,如果 new 分配内存失败,默认是抛出异常的(bad_alloc)。所以,如果分配成功,p == 0 就绝对不会成立;而如果分配失败了,也不会执行 if ( p == 0 ),因为分配失败时,new 就会抛出异常跳过后面的代码。如果你想检查 new 是否成功,应该捕捉异常:
try
{
int* p = new int[SIZE];
// 其它代码
}
catch ( const bad_alloc& e )
{
return -1;
}
或者使用set_new_handler函数处理new失败。set_new_handler的输入参数是operator new分配内存失败时要调用的出错处理函数的指针:
void nomorememory()
{
cerr << "unable to satisfy request for memory\n";
abort();
}
int main()
{
set_new_handler(nomorememory);
int *pbigdataarray = new int[100000000];
}
两者都是用于动态分配内存,malloc/free是C语言标准库的函数,new/delete是C++操作符,可以被重载。
new/delete的底层调用了malloc/free。
new分配内存按照数据类型进行分配,malloc分配内存按照大小分配。
new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL指针。
new和delete必须成对的使用,不能和malloc/free混合使用,这么写可能对于基本类型是没有问题的,但是一旦设涉及到构造函数与析构函数就不行了,因为malloc与free是C语言的标准库函数,并不负责构造和析构对象。而且标准库貌似没有规定new一定要使用malloc来实现,new是可以重载的,所以new的东西还是需要delete,而不是free。
【禁止产生堆对象】:
产生堆对象的唯一方法是使用new操作,所以我们可以禁止使用new。再进一步,new操作执行时会调用operator new,而operator new是可以重载的,所以可以让operator new和operator delete为private。
#include //需要用到C式内存分配函数
class Resource ; //代表需要被封装的资源类
class NoHashObject {
private:
Resource* ptr ;//指向被封装的资源
... ... //其它数据成员
void* operator new(size_t size){ //非严格实现,仅作示意之用
return malloc(size) ;
}
void operator delete(void* pp){ //非严格实现,仅作示意之用
free(pp) ;
}
public:
NoHashObject(){
//此处可以获得需要封装的资源,并让ptr指针指向该资源
ptr = new Resource() ;
}
~NoHashObject(){
delete ptr ; //释放封装的资源
}
};
NoHashObject现在就是一个禁止堆对象的类了,如果你写下如下代码:
NoHashObject* fp = new NoHashObject() ; //编译期错误!
delete fp ;
【禁止产生栈对象】:
我们可以用间接的办法完成,即让这个类提供一个static成员函数专门用于产生该类型的堆对象,而不是产生栈对象。
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance() {
return new NoStackObject() ;//调用保护的构造函数
}
void destroy() {
delete this ;//调用保护的析构函数
}
};
// 现在可以这样使用NoStackObject类了:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
// 对hash_ptr指向的对象进行操作
hash_ptr->destroy() ;
hash_ptr = NULL ; //防止使用悬挂指针
已分配空间出现内存越界访问,导致malloc函数的部分信息被破坏,造成下一次分配异常;
分配空间过大以致内存溢出;
已分配空间被当作内存碎片处理,暂时无法回收和利用,大量的内存碎片会造成系统剩余内存不足。
一般遇到这个问题有两种常见情况:
定义了较大的局部变量,导致超出了栈的最大内存。
递归调用的层次较深,由于在函数递归调用中,局部变量都要到递归结束后才能释放,所以很容易导致内存溢出。解决方法是在调用的过程中使用动态内存分配。
内存泄漏问题详见:内存泄漏及其检测方法、记项目中的一次内存泄漏问题。
详见:内存对齐的相关问题
参考:https://www.cnblogs.com/avril/p/3175175.html
https://blog.csdn.net/xxpresent/article/details/53024555