C++ 内存管理 之 STL内存分配实现原理

本文主要讲的是 各个C++版本 中的STL标准库中的默认内存分配器 的 各种实现

VC6 标准分配器实现

VC6 它什么内存管理都没做 直接调用的 operator newoperator new 内部实际上 调用的就是 malloc

此外 VC6 下 是以元素为单位 比如 int 的话 他就会分配 512 ints 如果是 double 则会分配 512 doubles

GNU-C++ 2.9 以字节为单位 GNU-C++ 4.9 以元素为单位 在 G2.9 中的容器默认分配器 被移到了 pool_allocator

注意 G2.9 的标准分配器 和 容器默认分配器 不一样噢!

GNU-C++ 2.9 标准分配器实现

std::allocator 标准分配器 也是什么都没做

但是 G2.9 容器使用的分配器 不是上面的那个 std::allocator 而是 std::alloc

GNU-C++ 4.9 标准分配器实现

也是什么都没做… 没有任何的特殊设计

G4.9 里有很多扩展的分配器 其中 G2.9 中的 std::alloc 变成了 __pool_alloc

因此 用 vector<string, __gnu_cxx::__pool_alloc<string>> vec; 能够节约不少 内存分配的内碎片 因为之前说过 malloc() 分配出来的内存 是有记录 当前区块大小的开销

GNU-C++ 2.9 std::alloc 原理

首先维护 16条自由链表 free_list[16] 掌控着16种不同小内存的分配 如果 超出了这16种小内存即 超过128字节的时候 则 交给 malloc() 去分配

16种小内存 分别为 8的倍数 如果小于8的倍数 会将其调整为 8 的倍数的边界

当 外界使用 std::alloc 分配内存时 每次 std::alloc 会获取 20 * 2 个 size 那么大的大内存块 其中 一份 20 进行切割为一个单向链表 剩下一份 20 会保留下来 以便下次使用

1
2
3
4
5
比如 我向 std::alloc 分配 32 字节的内存 
此时 std::alloc 会去获取 20 * 2 个 32个字节 + RoundUp(目前已申请的量 >> 4)追加量 那么大的内存块
其中 20 个 32个字节 会切割成单向链表 然后返回头一块回去
剩下的 20个 会保留起来 不做切割 假如未来我向 std::alloc 要 64个字节的内存的时候 就会从 保留的20个32个字节那么大的内存块里面切割
相当于切成了 10份 也是一样 返回头一块

换句话说 就是 如果没有预备内存的话 就会去申请 每次申请 20 * 2 * size + [RoundUp(目前已申请的量 >> 4)] 追加量 那么大的内存块 下次申请 如果有预备内存就直接用保留下来的那一份 如果没有了 则再次按照一开始的设定去申请一整块大内存

RoundUp() 函数内部是将其调整为 8 的字节

此外 就算预备内存足够大 也不会切超出 1~20块内存 永远在 20 以内

当系统内存不够用

系统内存不够用了 就从所需内存的右侧链表找 如果找的到 就直接将其一块小区块 注入 内存池中 再将其分配出去

如果所有的右侧都没有了 那就回到一级适配器 给 new handler 处理 看看还有没有一些补救措施 释放掉一些内存给我们用

如果还是不行 那就

第一级配置器

当第二级配置器失败了 就交给 第一级配置器 模拟 new handler 不断循环 想办法挖出更多的内存

但是 C++ 本身就有这种机制 GUN-C++ 4.9 不再有这段了

第二级配置器

第二级配置器就是我们上面讲的那个过程的配置器

总结 G2.9

这套内存分配器 通过维护一个内存池和16个自由链表 来减少 malloc 的调用 从而减少 malloc 时所浪费掉的内存(用于记录内存块大小)

可能有的人会觉得 就占用这一点内存没什么 但是 如果大批量的 malloc 小内存的话 那记录内存块大小的消耗就非常可观了.

但是也有缺陷 这一套方案 并没有 free 掉自己所占有的内存 而是将其存回自己的自由链表中 一方面是 很难 free 因为链表到最后全都乱掉了 只有地址相邻 大小相等 才能合并起来 最后再还回去.

因此 这可能也是为什么 G4.9 的时候 容器的标准内存分配器 不用这一套的原因 而是 采取什么都不做的形式 来分配内存.

此外 我们知道 当内存池 或者 自由链表的区块不够了 就会去 malloc 但是 每次 malloc 都是取 20 * 2 * size + 递增量 那为什么不每次 除以 2 想尽办法把 系统内存全部扣光呢? 可能是为了 把那些内存让给其他进程吧..

代码实现可见 Memory_Pool