经典图片
1. 申请内存的方式
new做以上三个事情,1申请一片内存,然后调用构造函数。
std::string 对应与stl中的实现类名为basic_string,其中为了实现reference count的功能,所以重载了operator new成员函数,使得每次申请的内存会比实际大小多一个extra的长度
delete做两个事情,1调用析构函数,2调用free释放内存
左边其实不会内存泄露,因为free的时候需要管理的长度其实记录在cookie里面,所以即使没有使用delete [] 也不会导致内存泄漏,当使用delete时,会正确释放对应长度的内存块,但是只会调用一次析构函数,因为Complex这个类的析构函数没有实际意义,所以不会导致内存泄漏;
而右边string array会导致内存泄漏如果使用delete,而不是delete[],因为string类的析构函数会释放类内的一个指针,所以如果没有调用数组中所有对象的析构函数,那么就会发生内存泄漏
new 解析重载
61h是错误的,正确的是60h,这是一个10进制数
计算方式如上所示:4+32+4+12*3+4+12+4=96=60h
pad是为了做16的倍数对齐
注:vc6.0 malloc的内存布局实际图如下:
通过重载成员函数operator new和operator delete,可以去掉cookie,节省内存使用,另外也可以减少malloc的调用,这种方式其实就相当于实现一个简单的内存池:
以上版本1----->到版本4的过程就是operator new到stl种allactor中发展演进的大致过程,一种标准库的allocator实现,这个可以匹配所有的类大小,有10种大小类型的链表,供选择
2. 标准库std::allocator的实现
优秀的设计——G2.9 std::alloc运行模式
- 每个节点都是8个字节的倍数,所以申请内存的时候最好进行8字节对齐
- 大概运行流程如下:(更详细的过程请看下文)
当有容器使用时,申请内存会调用一次alloc::allocator,那么会根据单个元素的大小,申请对应的20*2个元素大小的连续内存空间;(20是团队经验值,没有具体的源头,2是每次申请两倍的内存空间,一半划分为20个元素大小空间,使用链表链接起来,另外一半待下次有不同类型元素需要内存是供其使用)
如上图,假设第一个vector被使用时,T的类型是32字节,那么在free_list[16]的坐标为3的位置,会使用malloc申请出20*2*32字节的内存空间,其中起始20个被划分为32字节一块提供给vector使用,剩下一半作为备用;然后第二个容器list被使用,T的类型是64字节那么,那么会先使用free_list中剩余的没被使用的空闲内存,在坐标为3的位置,会将上次分配的20*32字节,划分为10*64的区间供list使用;紧接着第三个容器deque被使用时,T的类型是96字节,那么由于当前free_list没有空闲内存,那么会在坐标为11的位置申请20*2*96大小的内存空间,其中一半被划分为20*96的区块,供deque使用,剩下一半作为备用池子供下次使用
空闲链表的头会以union的结构作为头指针,如果被占用,移向下一个位置
std::alloc运行顺序
注意这里追加量会越来越大,计算方式是(累计量/16)
注意这里产生一个内存碎片, 会被安全链接到对应的节点位置,作为一个内存块,这样就安全处理了这一内存碎片
以上皆为正常流程,从下一页开始出现异常,无法申请更多的内存
std::alloc源码分析
临时变量会被拷贝到容器申请得到的空间上
- 1处与4处: 建议写成两行,否则让人难以理解
- 2处:值得学习,比较运算符右值放到左边
- 3处:变量最好在使用时的上一行定义,这样可以防止一些bug的出现
- 5处:是站在其他进程的角度考虑,才有它注释说的大灾难
- 6处(5下面,忘记标注了):系统中没有使用free,当alloc失败的时候,会导致有很多空闲的内存仍然没有被程序使用,如上图中第十三步中的白色区块
重载global operaot new operator delete
malloc & free
VC6 & VC10内存分配一览
注: 左边是main函数调用堆栈,关于main函数调用前后做了啥,看这里哦
1016是因为 有8个字节是给cookie预留
Small Block Heap ——SBH开始
HeapCreate是windows系统分配内存的api,这里会分配10个header
bitvEntryHi和bitvEntryLo组成一个64位的bitmap
其他的两个指针会作为内存大表重要的一部分,具体可见下图 VC6内存分配源码-6
这一个header未来会管理1MB的内存
这里是所有c++进程main函数的入口,所有的程序一进来就会分配32*8=256字节内存
这里对要分配的256字节的内存空间,外层套了一个壳子(调整实际需要分配的内存空间的大小为256+48(36转化为16的倍数为)=304,16进制表示为0x130),用于存储调试用的信息(DEBUG模式专属)
少画一格,四个字节,用来做16倍数对齐
0x131为cookie,本来为0x130,加1代表被使用了
1016是因为 有8个字节是给cookie预留
这里调整大小为16的倍数大小,同时这里的大小包含cookie(2*sizeof(int))
最上面带两个点的图代表上图tagheader这个数据结构,这俩点就表示两个指针,一个指向group组用来管理所有的内存块,一个指向实际的虚拟内存地址空间
tagRegion中indGroupUse代表当前分配到第几个group
bitvGroupHi和bitvGroupLo组合成为32*64位,对应32个group
grpHeaderList为32个group,数据结构为TagGroup
TagGroup中cntEntries代表当前group分配了多少次内存,之后为64个TagListHead
TagListHead里面为指向内存区间的两个指针,连为双向链表
从而最终有64个链表,也就是32对双向链表,同一节点用来链接同样大小的内存区间
1MB先分为8*32页,每页4k大小,8页为一组,被一个group管理
1MB内存的管理中心,分为32个group,从16字节到1k,1k以上的内存块都链接到最后一个grpHeaderList[31]
0xffffff为-1,作为分隔符使用,减去这个空间之后,4096-8=4088个字节,但是为了16字节对齐,所以保留8个字节,这8个字节完全浪费掉了,只是为了对齐,剩下4080大小的空间待被分配使用
响应第一个分配内存的请求,分配0x130字节空间,之后内存分布如上图所示
00000002标志位代表内存分配请求的来源,所以可以通过次标志位判断是否有内存泄漏,main函数运行完,归还内存时如果检查到有此位为00000001的那么就是发生了内存泄漏
通过一个count,也就是上面VC6内存分配源码-6 中taggroup数据结构内的cntEntries,这个字段来判断,申请一次内存这个值+1,释放一次内存这个值-1;(类似的设计在其他编译器的内存分配中同样出现)
全回收后内存回到VC6内存分配源码-7 中初始的状态
一整块1MB内存被全回收后,被不会立刻被操作系统回收,而是等待下次被使用,直至有第二个全回收的1MB内存,才会回收,因此会至少保留一块1MB作为预留使用(使用指针__sbh_pHeaderDefer保存)
这些系统函数可以分析sbh内存分配管理系统
malloc速度并不慢,因为sbh很优秀
std::allocator存在的意义并不是malloc不够快,一部分原因是为了节约cookie,相当于节约内存
为什么图中每一层都有内存管理,是不是冗余的,是不是浪费?
答案是浪费的,是冗余的,但是确实有必要,因为c是跨平台的,自己是完整的系统,同样c++也是有自己完整的系统,不依赖于自己的底层, 每一层都互相不依赖,不能预设其他层次有优秀的内存管理存在,可以保证自己内存管理是优秀的,性能是高的,因为每一层都使用自己优秀但是类似的内存池管理设计。
Loki Library 中内存管理实现的学习
这是一个关于设计模式的库,但是不是很成熟,维护度不高,但是作者是《Modern c++ Design》的作者
Loki 中的allocator的实现方式基于上述三个类,最底层的类是Chunk,暴露给用户的类是SmallObjAllocator,这三个类在一个.h和一个.cpp文件中非常精简,其中Chunk类是FixedAllocator类的inner类
其中使用的数据结构也如上图所示,其中最底层chunk 中pdata指向实际要管理的一块内存,firstAvailableBlock_用来表示接下里要供应的区块号的索引,blocksAvailable表示当前chunk中还有多少区块可以使用(相当于sbh中的count计数,用来判断当前chunk内存使用的状态,是全部归还了,还是全部被使用了)
使用每一个block的第一个字节赋值的初始索引值,用来代替sbm中的链表
这里只判断当前block是否还有内存可以分配,同时修改chunk类内数据成员的状态
归还的时候先判断当前内存是在哪个chunk,然后调用chunk的deallocator函数归还内存,
这里有两个特殊指针,作为标兵:allocChunk和deallocChunk;;
allocChunk用来表示下次优先考虑提供内存的chunk,因为默认会觉得上次这个chunk可以提供内存,那么下次大概率也可以
deallocChunk用来表示上次内存回收到这个chunk,那么下次大概率可能也会回收到这个chunk
临近查找法,其实看起来就是二路查找法,兵分两路查找
回收时同样会判断是否是全回收,同样,全回收之后会预留一个以供不时之需,不会还给操作系统
同时这里存在一个bug?(来找茬,反正我没找到)