文章目录
内存管理
0. 概述
- 常见内存使用错误(debug注意事项)
- 内存
分配未成功却使用
。内存分配不一定总是成功的,使用需要进行防御性编程if(p != NULL)
。 - 内存
分配成功却未初始化就使用
。内存使用前要初始化,否则指针访问行为不可控, - 内存
访问越界
。读越界,若读的内存地址无效,则程序可能崩溃。写越界可能出现shellcode攻击 释放内存却继续使用
- 函数return局部变量
- 使用free或delete释放了内存后,没有将指针设置为NULL,导致产生“悬空指针”
- 内存
1.1 new关键字
- 作用:在C++中用于动态分配内存,可通过重载
operator new函数
改变new关键字的行为 - 三种用法
- plain new
- 最朴素的new
- 进行内存的分配、转型和初始化,在分配失败时抛出异常并返回
bad_alloc
- nothrow new
- 在空间分配失败的情况下是
不抛出异常
,而是返回NULL
。 - 常用于
服务器端内存不足
时,不抛出异常而使程序终止,而是等待内存分配
- 在空间分配失败的情况下是
- placement new:
- 原理:通过重载operator new,调用构造函数初始化指定内存空间,从而实现对已分配内存的复用。
- 优点:复用已分配的内存,降低分配释放内存开销并减少内存碎片问题
- plain new
class A {
int m_v;
public:
A() {}
A(int v) : m_v(v) {}
A(double v) : m_v(ceil(v)) {}
};
// plain new
A* p1 = new A; //非必要情况不会调用合成的构造函数
A* p2 = new A(); //必然调用构造函数,如果没有,调用合成构造函数
A* p3 = new A(3); //调用int参数构造
A* p4 = new A(3.1); //调用double类型构造
A* p5 = new A[2]; //分配2个元素数组,非必要不初始化
A* p6 = new A[2](); //分配2个元素,初始化
A* p7 = new A[2](3); //非法调用,动态分配数组不能指定带参数的构造函数
delete p1;
delete p2;
delete p3;
delete p4;
delete [] p5;
delete [] p6;
// nothrow new
p1 = new(nothrow) A;
if(p1 == nullptr)
···;//等待并重试
// placement new
// 1. 申请内存
void* pbuffer = (void*) new char[sizeof(CTest)];
// 2. 在指定的内存空间上构造对象
CTest* ptest = new (pbuffer) CTest();
// 3. 使用对象
ptest->doing();
// 4. 显式调用析构函数
ptest->~CTest();
// 5. 释放原始的内存空间
delete[] pbuffer;
// 6.重复234进行循环使用
- 原理
- new本质:调用operator new
分配
内存,将该内存进行转型
后赋值给指针,然后调用构造函数进行内存的初始化
,如果失败返回bad_alloc
- operator new本质:尝试使用malloc进行分配内存,同时进行异常处理,可以重载
// ### 调用new Complex *pc = new Complex(1, 2); // ### new的原理解释(背诵) Complex *pc; try{ // 1.内存分配,调用operator new void *mem = operator new(sizeof(Complex)); // 2.内存转型,将分配的内存转化成相应的指针类型 pc = static_cast<Complex*>(mem); // 3.内存初始化,调用构造函数初始化内存 pc->Complex::Complex(1, 2); // 4.内存分配失败返回bad_alloc }catch(std::bad_alloc){ // 若allocator失败就不执行constructor } // ### operator new的源码概括性解释 // 第二参数保证函数不抛出异常 void *operator new(size_t size, const std::nothrow_t &){ void *p; // 如果内存耗尽导致分配失败 (实质调用malloc) while((p=malloc(size)) == 0){ _TRY_BEGIN if(_callnewh(size) == 0)// 调用自定义函数进行处理 break; _CATCH(std::bad_alloc) return 0; _CATCH_END } return p; }
- new本质:调用operator new
1.2 delete关键字
- 作用:
- 释放new分配的动态内存:delete需要与new配对使用,其参数可以是指向一块内存首地址或空指针(nullptr)。不能对同一块内存多次delete,但是可以对空指针多次delete。
- 阻止编译器合成默认函数:C++11以上,如果没有为类编写构造函数、析构函数、拷贝构造函数、移动构造函数,以及拷贝赋值运算符、移动赋值运算符,编译器可能会为类合成默认的函数版本。显式使用delete,可以阻止编译器合成对应函数。
- 本质:先调用类对象的
析构函数
,后调用operator delete函数进行内存释放
。operator delete函数本质是调用free函数// delete的编译解释,如delete pc; // 1.析构调用对象 pc->~Complex(); // 2.后释放对象 operator delete(pc); //operator delete源码 void __cdecl operator delete(void *p)_THROW0(){ free(p); } class MyObj { public: // 阻止编译器合成构造函数,会导致类无法实例化 MyObj() = delete; // 阻止编译器合成拷贝构造函数,会导致类无法拷贝构造 MyObj& MyObj(const MyObj &) = delete; // 阻止合成赋值运算符,会导致类无法使用赋值运算 MyObj& operator=(const MyObj &) = delete; ... }
- 不能对同一内存地址进行多次delete的原因
- 第一次delete:只是逻辑上释放p指向的内存(将内存池中该部分内存使用状态置为空闲)
- 第二次delete:仍然可以通过该内存地址进行逻辑置为空闲,再通过该悬空指针delete会导致内存错误
- 一旦我们释放了一个内存空间,必须保证不再通过任何其他残留的指针或变量能够访问到它
// delete的使用 int *p = new int(3); delete p; p = nullptr;// 重点
- new和delete的对称性
- new分配的空间使用delete释放,new[] 使用 delete[]
- 如果重载了operator new,要注意delete的使用,避免内存泄漏
- new和delete的组合使用
// 动态申请/释放一个int类型的空间 int* ptr1 = new int; delete ptr1; // 动态申请/释放一个int类型的空间并初始化为10 int* ptr2 = new int(10); delete ptr2; // 动态申请/释放10个int类型的空间 int* ptr3 = new int[10]; delete[] ptr3;
- delete是如何获知需要释放的内存(数组)大小的?
- 动态申请的内存块首部有cookie记录该块内存大小
2.1 malloc关键字
- 作用:申请一块连续的指定大小的内存块区域,以void*类型返回分配的
起始地址
- 类型
- malloc函数:从堆上
申请内存空间
,尽量使用memset初始化 - calloc函数:从堆上
申请内存空间并初始化为0
- realloc函数:
对已经存在的内存空间进行调整
,如果更大会进行内存空间的延申,如果无法延申会申请新空间并拷贝和释放旧空间。如果更小会将原空间缩小。
- malloc函数:从堆上
- 内存空间的释放
- 使用free函数
- 使用指针参数为NULL的realloc函数
- 原理
- 初始化内存块双链表
- 将堆空间在逻辑上分割为空闲内存块,并且双向链表进行管理,利用系统调用完成对内存的申请。
- 首部:链表内存块由
mem_control_block+有效内存块
组成,内存控制块主要包含pre指针、next指针、有效内存块大小、Used标志(是否使用) - 数据:有效内存块是实际承载数据的,malloc返回的是有效内存块的首地址。
- 查找空闲区块
- 首次适用算法First fit:遍历链表,使用第一个数据区大小大于要求size的块作为此次分配的块,具有更好的运行效率
- 最佳适应算法Best fit:遍历链表,使用数据区大小大于size且差值最小的块作为此次分配的块 ,较高的内存使用率
- 调用系统调用指令mmap或sbrk向系统申请新的虚拟内存满足用户需要,并将其加入链表管理。
- 分解空闲区块
- 将查找到的空闲内存块拆分,一部分设置used为已占用状态并返回该有效内存块首地址给程序使用,另一部分继续作为空闲块
// 首次适用查找算法的malloc实现 int has_initialized = 0; // 初始化标志 void *managed_memory_start; // 指向堆底(内存块起始位置) void *last_valid_address; // 指向堆顶 void malloc_init() { // 这里不向操作系统申请堆空间,只是为了获取堆的起始地址 last_valid_address = sbrk(0); managed_memory_start = last_valid_address; has_initialized = 1; } void *malloc(long numbytes) { void *current_location; // 当前访问的内存位置 struct mem_control_block *current_location_mcb; // 只是作了一个强制类型转换 void *memory_location; // 这是要返回的内存位置。初始时设为 // 0,表示没有找到合适的位置 if (!has_initialized) { malloc_init(); } // 要查找的内存必须包含内存控制块,所以需要调整 numbytes 的大小 numbytes = numbytes + sizeof(struct mem_control_block); // 初始时设为 0,表示没有找到合适的位置 memory_location = 0; /* Begin searching at the start of managed memory */ // 从被管理内存的起始位置开始搜索 // managed_memory_start 是在 malloc_init 中通过 sbrk() 函数设置的 current_location = managed_memory_start; while (current_location != last_valid_address) { // current_location 是一个 void 指针,用来计算地址; // current_location_mcb 是一个具体的结构体类型 // 这两个实际上是一个含义 current_location_mcb = (struct mem_control_block *)current_location; if (current_location_mcb->is_available) { if (current_location_mcb->size >= numbytes) { // 找到一个可用、大小适合的内存块 current_location_mcb->is_available = 0; // 设为不可用 memory_location = current_location; // 设置内存地址 break; } } // 否则,当前内存块不可用或过小,移动到下一个内存块 current_location = current_location + current_location_mcb->size; } // 循环结束,没有找到合适的位置,需要向操作系统申请更多内存 if (!memory_location) { // 扩展堆 sbrk(numbytes); // 新的内存的起始位置就是 last_valid_address 的旧值 memory_location = last_valid_address; // 将 last_valid_address 后移 numbytes,移动到整个内存的最右边界 last_valid_address = last_valid_address + numbytes; // 初始化内存控制块 mem_control_block current_location_mcb = memory_location; current_location_mcb->is_available = 0; current_location_mcb->size = numbytes; } // 最终,memory_location 保存了大小为 numbyte的内存空间, // 并且在空间的开始处包含了一个内存控制块,记录了元信息 // 内存控制块对于用户而言应该是透明的,因此返回指针前,跳过内存分配块 memory_location = memory_location + sizeof(struct mem_control_block); // 返回内存块的指针 return memory_location; } //对应的free实现: void free(void *ptr) { // ptr 是要回收的空间 struct mem_control_block *free; free = ptr - sizeof(struct mem_control_block); // 找到该内存块的控制信息的地址 free->is_available = 1; // 该空间置为可用 return; }
- 初始化内存块双链表
- malloc数据结构实现方式发展(增加伙伴分配和内存链表分配算法)
- 全链表
- 数据结构&算法:使用
双向链表
管理所有内存块,使用首次适应查找算法
查找合适空闲内存块进行分割分配 - 问题:易产生内存碎片,每次遍历要从头开始
- 数据结构&算法:使用
- 空闲链表
- 数据结构&算法:维护一个只包含未分配内存块的
空闲块链表
,使用首次适应查找算法
查找合适空闲内存块进行分割分配,剩余空间还是存在空闲链表中 - 问题:无法使用内存紧凑,因为可能改变之前malloc返回的地址
- 数据结构&算法:维护一个只包含未分配内存块的
- 多空闲链表(目录思维)
- 特点:维护多个大小不同的空闲链表,一般是2的指数递增。先选择合适的空闲链表,后进行遍历。
- tcmalloc
- 来源:tcmalloc 是 Google 开发的内存分配器,全称 Thread-Caching Malloc,即线程缓存的 malloc
- 原理:利用池化思想管理内存分配。对于每个线程,都有自己的私有缓存池,内部包含若干个不同大小的内存块。对于一些小容量的内存申请,可以使用线程的私有缓存;私有缓存不足或大容量内存申请时再从全局缓存中进行申请。
- 优点:在线程从自己内存池中申请内存不需要加锁,因此在多线程的情况可以大大提高分配效率。
- 全链表
- 其他相关
- 编译映射:现在的 malloc() 往往采用多种方式复合而成,不同大小的内存块往往采用不同的措施,以保证内存分配的安全和效率。
- 缓冲池思想:先通过sbrk函数扩展堆,将这部分空闲内存空间作为缓冲池,然后通过 malloc / free 管理缓冲池中的内存,能够避免频繁的系统调用,提高程序性能。
- 使用双链表而不是单链表的原因:进行内存碎片整理时,可以更快的合并相邻的碎片
- 堆的控制:malloc和free是通过系统调用brk实现的,sbrk也是基于brk实现的。通过改变堆顶指针而实现堆的容量控制
- 大内存块的分配
- mmap() 系统调用可以在进程的虚拟地址空间中分配一块连续的内存区域,并返回该内存区域的首地址。
- mmap() 分配的内存在 malloc() 管理的内存池中不可见,因此不能使用 free() 函数来释放,必须使用 munmap() 函数来释放。
- 由于 mmap() 函数需要进行系统调用,因此在频繁调用时可能会带来一定的性能损失,需要谨慎使用。
2.2 free关键字
- 作用:释放指针指向的内存,即将对应内存块控制信息中的Used标识置0
- 注意点
- 避免野指针:释放完成后,将指针置nullptr
- free只能对自己管理内存池进行逻辑释放
内存泄漏
- 定义:动态申请的内存空间
用完未释放
- 常见原因
- 唯一指针改变指向:动态分配内存的未释放而唯一指向的指针被重新赋值
- 唯一指针被释放:释放动态申请的结构体,而未释放其含有动态分配指针的子元素
- 返回的动态内存没指针接收:函数返回动态分配内存而未处理
- 僵尸进程占用资源无法释放
- 解决方式
- 预防:针对上述三个错误,进行校验核对。使用智能指针
- 检测:内存泄露检测工具valgrind、BoundsChecker
- 解除:抛出异常给上层处理
- strcpy函数和strncpy函数
- 函数原型
char* strcpy(char* strDest, const char* strSrc) char *strncpy(char *dest, const char *src, size_t n)
- strcpy函数: 如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。
- strncpy函数:用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。
- 函数原型
垃圾回收机制
- 定义:程序员仅内存的申请和使用,而由内存管理器负责释放不再使用的内存空间
- 缺点及解决方式
- 统一的垃圾回收机制,为适应不同环境而导致程序性能降低
- 会减弱C++对底层的控制
- 可以使用智能指针进行解决
STL的内存池Memory Pool
1.什么是内存池
- 池化技术
- 原理:将程序中经常使用的一个核心资源
预先申请
出来,放到一个池里,提高资源的利用率,减少系统调用
的使用 - 示例:内存池、线程池
- 原理:将程序中经常使用的一个核心资源
- 碎片问题
- 原因:长时间的申请和释放内存,可能造成大量的内存碎片,从而降低性能。
- 解决:预申请一定数量的内存块作为内存池,程序的申请和释放内存通过内存池实现。内存池不够时申请更大的内存,减少内存碎片。
2.如何使用内存池
-
SGI版本的STL内存池实现原理
- SGI使用双层级配置器
- 第一层级仅是
对malloc和free的简单封装
,用于程序申请大于128B
的内存时调用,通过系统调用sbrk动态修改进程的堆段段顶指针 - 第二层级内置一个
轻量级的内存池
,当程序申请小于128B
的内存时会被调用
- 第一层级仅是
- 第二级配置器是为了解决小区块申请释放导致的内存碎片化问题
- SGI默认最大的小块内存大小为128bytes,并维护了16 个空闲链表(free list),每个list 分别维护大小为 8, 16, 24, …, 128bytes 的内存块(均为8的整数倍),
- 如果有足够的小区块,则调整链表,返回第一个node, 链表头改为第二个 node。其中如果用户申请的空间大小不足8B的倍数,则向上取整。
- 如果没有可用的区块,计算内存池容量,尽最大能力交付,不足向系统申请。释放内存时, 如果大于128bytes, 则直接 free, 否则加入相应的自由链表中而不是直接返还给操作系统。
- 如果整个堆的空间都不够了,就会在原先已经分配区块中寻找能满足当前需求的区块数量,能满足就返回,不能满足就向客户端报bad_alloc异常
- SGI默认最大的小块内存大小为128bytes,并维护了16 个空闲链表(free list),每个list 分别维护大小为 8, 16, 24, …, 128bytes 的内存块(均为8的整数倍),
- freelist中的联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。
union obj { union obj * free_list_link; //下一个节点的指针 char client_data[1]; //内存首地址 }
- SGI使用双层级配置器
-
高性能的tcmalloc内存池
- https://www.zhihu.com/search?type=content&q=tcmalloc%E5%8E%9F%E7%90%86
- https://www.zhihu.com/search?type=content&q=tcmalloc%E5%8E%9F%E7%90%86
3.内存池的原理
- 内核的分页机制
- 进程的地址空间都是虚拟地址
- 虚拟地址的范围取决于地址总线的宽度,eg:32 位下,虚拟地址空间为4GB
- 每个进程拥有各自完整的虚拟地址空间,eg:32 位下,每个进程都拥有4GB
- 物理内存池分为两部分
- 内核物理内存池:物理内存只给操作系统使用,用户程序无权访问
- 用户物理内存池:当用户进程消耗尽时,不再向内核内存池申请