主要在于:内存的申请 与 内存的释放(归还)
申请内存
释放内存
一.申请内存
ThreadCache
- Allocate: 拿一个小块内存给程序.1.先找到对应的freeList拿 2.找CentralCache拿
- FetchFromCentralCache:向CentralCache索要"批量".若要到了多个将多的挂在对应的freeList
CentralCache
- FetchRangeObj: 获取批量小块内存给ThreadCache, .
- 若不够批量则有多少拿多少, 返回实际拿到的小块内存数量
- 小块内存来自Span对象--调用GetSpan
- GetSpan: 获取一个Span对象 1.在SpanList里拿 2. 找PageCache拿
- 将拿到的大块内存切分成小块, 并挂到Span对应的freeList
- 返回拿到的Span对象
PageCache
- NewSpan: 1.在当前桶里拿 2. 找后面的桶拿 3.找堆拿
- 拿到的Span, 需要记录PageID与Span的映射关系.后面回收小块还给对应的Span
- 在后面桶拿到的Span要做切割, 额外还要记录剩下Span首尾页与Span的映射.后面合并相邻页
- 返回这个Span对象
二.释放内存
ThreadCache
- 找到对应的_freeList, 头插
- 若_freeList过长, 还一段内存给CentralCache. 调用ListToLong
CentralCache
- ReleaseListToSpans.根据之前PageID与Span建立的映射关系, 找到Span,进行头插
- 若use_count == 0. 说明所有小块内存回来了. 将其还给PageCache.
PageCache
- ReleaseSpanToPageCache: 根据之前建立的映射关系(首尾页).合并相邻的页 + 记录该Span首尾页与Span的映射关系. 方便其它来合并
三.细节及优化
使用基数树来建立PageID与Span对象的映射会更快
- 读取的时候可以不加锁
基数树首先就开好了空间(写入操作不会改结构). 所以读的时候不用加锁
而使用hash表存其映射, 若进行写, 可能回导致底层结构变化(扩容),所以必须加锁
- 高度更低(1~3层)
32位下, 记录页数 = 2^32 / 一页的大小(2^13) = 2^19. 能直接开出来, 但64位就不能
第一二层时预先分配的(尽可能覆盖大多数地址空间) 第三层按需分配来节省空间
四.可能的BUG预防
内存泄漏
- 由于还内存不是还_freeList的所有小块对象(也不该还所有), 所以可能导致部分内存块一直挂在_freeList上.(但是进程结束之后, 会自动释放, 就没有内存泄漏了).
- 若程序不会退出, 则考虑ThreadCache上的内存块到一定大小, 调用函数, 将其归还
双重释放
- 外部使用的时候, 释放完将指针置nullptr, 内部有assert断言操作(为nullptr直接报错)
- 使用智能指针管理内存,配合定制删除器
#include <iostream>
#include <memory>
// 自定义new和free函数
void* myNew(size_t size) {
return ConcurrentAlloc(size);
}
void myFree(void* ptr) {
ConcurrentFree(ptr);
}
// 定义自定义删除器
struct CustomDeleter {
void operator()(int* p) {
if (p) myFree(p);
}
};
int main() {
// 使用智能指针管理动态分配的内存
std::shared_ptr<int> ptr(static_cast<int*>(myNew(sizeof(int))), CustomDeleter());
return 0;
}