【高并发内存池】第七篇:回收内存过程调通

简介: 该头文件中包含公共的数据结构、方法、常量等。

一. 回收内存主流程梳理

185497c9bfc245aca275fab5d6ea0ef0.png


二. 完整代码


1. Common.h


该头文件中包含公共的数据结构、方法、常量等。


#pragma once
#include <mutex>
#include <thread>
#include <iostream>
#include <assert.h>
#include <algorithm>
#include <unordered_map>
using std::cout;
using std::endl;
static const size_t NPAGES = 129;   // PageCache可申请的最大页数
static const size_t PAGE_SHIFT = 13;  // 通过移位运算计算页号和页的起始地址
static const size_t NFREELIST = 208;        // 小块定长内存自由链表桶的数量
static const size_t MAX_BYTES = 256 * 1024; // ThreadCache可申请的最大内存空间
// 条件编译不同平台、系统的页号类型
#ifdef _WIN64
  typedef unsigned long long PAGE_ID;
#elif _WIN32
  typedef size_t PAGE_ID;
#else 
  //Linux
#endif
// 条件编译不同系统的系统头文件
#ifdef _WIN32
  #include <windows.h>
#else
  // Linux
#endif
// 条件编译不同系统到堆上以页为单位申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
  void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
  // linux下brk mmap等
#endif
  if (ptr == nullptr)
  throw std::bad_alloc();
  return ptr;
}
// 返回传入空间的头4个或8个字节内容的引用
static inline void*& NextObj(void* obj)
{
  assert(obj);
  return *(void**)obj;
}
// 管理切分好的小块定长内存的自由链表
class FreeList
{
public:
  // 头插一个小块定长内存
  void PushFront(void* obj)
  {
  assert(obj);
  NextObj(obj) = _freeList;
  _freeList = obj;
  ++_size;
  }
  // 头删一个小块定长内存
  void* PopFront()
  {
  assert(!Empty());
  void* obj = _freeList;
  _freeList = NextObj(obj);
  --_size;
  return obj;
  }
  // 头插批量小块定长内存
  void PushRangeFront(void* start, void* end, size_t n)
  {
  assert(start);
  assert(end);
  assert(n > 0);
  NextObj(end) = _freeList;
  _freeList = start;
  _size += n;
  }
  // 头删批量小块定长内存
  void PopRangeFront(void*& start, void*& end, size_t n)
  {
  assert(n <= _size);
  start = _freeList;
  end = _freeList;
  // 让end走到最终要删除的那个位置
  for (size_t i = 0; i < n - 1; ++i)
  {
    end = NextObj(end);
  }
  _freeList = NextObj(end);
  NextObj(end) = nullptr;
  _size -= n;
  }
  // 判断自由链表是否为空
  bool Empty()
  {
  return _size == 0;
  }
  // 获取该链表像中心缓存申请内存时的慢启动调节值
  size_t& GetAdjustSize()
  {
  return adjustSize;
  }
  // 获取自由链表中小块定长内存的数量
  size_t GetSize()
  {
  return _size;
  }
private:
  size_t _size = 0;     // 小块定长内存的数量
  size_t adjustSize = 1;     // 向中心缓存申请内存的慢启动调节值
  void* _freeList = nullptr; // 存储小块定长内存的自由链表
};
// 计算传入对象大小和小块定长内存的对齐映射规则
class SizeClass
{
  // 整体控制在最多10%左右的内碎片浪费
  // [1,128]        8byte对齐      freelists[0,16)
  // [128+1,1024]       16byte对齐      freelists[16,72)
  // [1024+1,8*1024]      128byte对齐      freelists[72,128)
  // [8*1024+1,64*1024]    1024byte对齐     freelists[128,184)
  // [64*1024+1,256*1024]  8*1024byte对齐   freelists[184,208)
public:
  // 计算对齐后得到的定长块大小
  static inline size_t RoundUp(size_t bytes)
  {
  if (bytes <= 128)
  {
    return _RoundUp(bytes, 8);
  }
  else if (bytes <= 1024)
  {
    return _RoundUp(bytes, 16);
  }
  else if (bytes <= 8 * 1024)
  {
    return _RoundUp(bytes, 128);
  }
  else if (bytes <= 64 * 1024)
  {
    return _RoundUp(bytes, 1024);
  }
  else if (bytes <= 256 * 1024)
  {
    return _RoundUp(bytes, 8 * 1024);
  }
  else
  {
    assert("ThreadCache over 256k\n");
    return -1;
  }
  }
  // 计算映射到哪一个自由链表桶
  static inline size_t Index(size_t bytes)
  {
  // 记录每一个对齐区间有多少个桶
  static int group_array[4] = { 16, 56, 56, 56 };
  // 根据传入的对象大小映射得到自由链表桶对应的下标
  if (bytes <= 128) {
    return _Index(bytes, 3);
  }
  else if (bytes <= 1024) {
    return _Index(bytes - 128, 4) + group_array[0];
  }
  else if (bytes <= 8 * 1024) {
    return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
  }
  else if (bytes <= 64 * 1024) {
    return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
  }
  else if (bytes <= 256 * 1024) {
    return _Index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
  }
  else {
    assert("ThreadCache over 256k\n");
    return -1;
  }
  }
  // ThreadCache一次从中心缓存批量获取小块定长内存数量的上、下限
  static size_t LimitSize(size_t alignBytes)
  {
  assert(alignBytes != 0);
  // [2, 512],一次批量移动多少个对象的(慢启动)上限值
  // 大对象(最大256KB)一次批量下限为2个
  // 小对象(最小8字节)一次批量上限为512个
  size_t num = MAX_BYTES / alignBytes;
  if (num < 2)
    num = 2;
  if (num > 512)
    num = 512;
  return num;
  }
  // 计算一次向系统获取几个页合适
  // 单个对象 8byte --- 分给CentralCache一个1页的Span
  // ...
  // 单个对象 256KB --- 分给CentralCache一个64页的Span
  static size_t NumMovePage(size_t alignBytes)
  {
  size_t num = LimitSize(alignBytes);
  size_t npage = num * alignBytes;
  npage >>= PAGE_SHIFT;
  if (npage == 0)
    npage = 1;
  return npage;
  }
private:
  static inline size_t _RoundUp(size_t bytes, size_t alignNum)
  {
  return ((bytes + alignNum - 1) & ~(alignNum - 1));
  }
  static inline size_t _Index(size_t bytes, size_t align_shift)
  {
  return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
  }
};
// Span管理以页为单位一个的大块跨度内存
struct Span
{
  size_t _n = 0;       // 页的数量
  PAGE_ID _pageId = 0; // 大块跨度内存起始页的页号
  Span* _next = nullptr; // 指向双向链表中前一个大块跨度内存
  Span* _prev = nullptr; // 指向双向链表中后一个大块跨度内存
  size_t _useCount = 0;     // 被分配给ThreadCache的小块定长内存计数
  void* _freeList = nullptr;// 存储切好的小块定长内存的自由链表
  bool _isUse = false; // 是否在被使用
};
// 管理span的带头双向循环链表
class SpanList
{
public:
  // 构造函数中new一个哨兵位的头结点
  SpanList()
  {
  _head = new Span;
  _head->_prev = _head;
  _head->_next = _head;
  }
  // 在pos位置插入一个新的大块跨度内存
  void Insert(Span* pos, Span* newSpan)
  {
  assert(pos);
  assert(newSpan);
  // 1、记录前一个节点
  Span* prev = pos->_prev;
  // 2、newSpan插入到中间位置:prev newSpan pos
  prev->_next = newSpan;
  newSpan->_prev = prev;
  newSpan->_next = pos;
  pos->_prev = newSpan;
  }
  // 移除pos位置的一个大块跨度内存
  void Erase(Span* pos)
  {
  assert(pos);
  assert(pos != _head); // 不能删除哨兵位的头结点 
  // 1、拿到pos位置的前后节点
  Span* prev = pos->_prev;
  Span* next = pos->_next;
  // 2、连接前后节点,pos不用delete这里只负责把它移出链表,外部会还对pos做处理
  prev->_next = next;
  next->_prev = prev;
  }
  // 获取第一个Span
  Span* Begin()
  {
  return _head->_next;
  }
  // 获取哨兵位头结点
  Span* End()
  {
  return _head;
  }
  // 判空
  bool Empty()
  {
  return Begin() == _head;
  }
  // 头删一个span对象并返回给外部
  Span* PopFront()
  {
  assert(!Empty());
  Span* ret = Begin();
  Erase(Begin());
  return ret;
  }
  // 头插一个span对象
  void PushFront(Span* span)
  {
  assert(span);
  Insert(Begin(), span);
  }
  // 获取桶锁
  std::mutex& GetSpanListMutex()
  {
  return _mtx;
  }
private:
  Span* _head;  // 哨兵位的头结点
  std::mutex _mtx; // 桶锁
};



2. ThreadCache相关文件


ThreadCache.h:包含线程缓存类的声明和创建线程自己的TLS。


#pragma once 
#include "Common.h"
// ThreadCache本质是由一个哈希映射的已切分好的小块定长内存组成的自由链表集合
class ThreadCache
{
public:
  // 申请一个小块定长内存
  void* Allocate(size_t bytes);
  // 释放一个小块定长内存
  void Deallocate(void* ptr, size_t bytes);
private:
  // 归还一段list给CenralCache
  void ListToolong(FreeList& list, size_t bytes);
  // 从中心缓存获取批量小块定长内存,并返回其中一个
  void* FetchFromCentralCache(size_t index, size_t alignBytes);
  FreeList _freeLists[NFREELIST]; // 哈希桶
};
// TLS thread local storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;


ThreadCache.cpp:包含线程缓存成员函数的实现。


#include "ThreadCache.h"
#include "CentralCache.h"
// 申请一个小块定长内存
void* ThreadCache::Allocate(size_t bytes)
{
  // ThreadCache只能申请小于等于256KB的对象空间
  assert(bytes <= MAX_BYTES);
  // 1、确定对齐后的小块定长内存大小以及它映射到那个自由链表桶
  size_t index = SizeClass::Index(bytes);
  size_t alignBytes = SizeClass::RoundUp(bytes);
  // 2、如果自由链表桶不空的话,把第一个小块定长内存取出来
  //    如果桶中没有小块定长内存,就到CentralCache中去批量申请
  if (!_freeLists[index].Empty())
  {
  return _freeLists[index].PopFront();
  }
  else
  {
  return FetchFromCentralCache(index, alignBytes);
  }
}
// 释放一个小块定长内存
void ThreadCache::Deallocate(void* ptr, size_t bytes)
{
  assert(ptr);
  assert(bytes <= MAX_BYTES);
  // 1、计算映射到哪一个自由链表桶
  size_t index = SizeClass::Index(bytes);
  // 2、把小块定长内存头插到自由链表桶中
  _freeLists[index].PushFront(ptr);
  // 3、当自由链表长度大于等于一次批量申请的数量时就归还当前自由链表的所有小块定长内存给CenralCache
  if (_freeLists[index].GetSize() >= _freeLists[index].GetAdjustSize())
  {
  ListToolong(_freeLists[index], bytes);
  }
}
// 从中心缓存获取“一批”小块定长内存,并返回给外部“一块”小块定长内存
void* ThreadCache::FetchFromCentralCache(size_t index, size_t alignBytes)
{
  // 慢启动反馈调节算法
  // 1、最开始不会一次向CentralCache批量太多,因为要太多了可能用不完
  // 2、每次批发的数量逐渐增多,直到上限
  // 3、alignBytes越小,一次向CentralCache要的batchNum就越大
  // 4、alignBytes越大,一次向CentralCache要的batchNum就越小
  assert(index >= 0);
  assert(alignBytes >= 8);
  // 1、计算需要向CentralCache申请的小块定长内存的数量
  size_t batchNum = min(_freeLists[index].GetAdjustSize(), SizeClass::LimitSize(alignBytes));
  if (_freeLists[index].GetAdjustSize() == batchNum)
  {
  _freeLists[index].GetAdjustSize() += 1;
  }
  // 2、传入输出型参数向CentralCache申请批量小块定长内存
  void* begin = nullptr;
  void* end = nullptr;
  size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(begin, end, batchNum, alignBytes);
  // 3、返回给外部第一个小块定长内存,其它的挂到自由链表桶中
  assert(actualNum > 0);
  if (actualNum == 1)
  {
  assert(begin == end);
  return begin;
  }
  else
  {
  _freeLists[index].PushRangeFront(NextObj(begin), end, actualNum-1);
  return begin;
  }
}
// 归还一段list给CenralCache
void ThreadCache::ListToolong(FreeList& list, size_t bytes)
{
  assert(bytes > 0);
  void* start = nullptr;
  void* end = nullptr;
  // 1、把自由链表中的所有小块定长内存剥离
  list.PopRangeFront(start, end, list.GetSize());
  // 2、只需要传第一个小块定长内存的指针即可,因为它们是单链表结构组织起来的,最后会走到空
  CentralCache::GetInstance()->ReleaseListToSpans(start, SizeClass::Index(bytes));
}


3. CentralCache相关文件


CentralCache.h:包含中心缓存类的声明。


#pragma once
#include "Common.h"
// 饿汉的单例模式
class CentralCache
{
public:
  // 返回CentralCache的单例对象
  static CentralCache* GetInstance()
  {
  return &_sInst;
  }
  // 从中心缓存获取一定数量的小块定长内存给ThreadCache
  size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t alignBytes);
  // 回收自由链表中的所有小块定长内存到它们各自对应的Span中
  void ReleaseListToSpans(void* start, size_t index);
private:
  // 从特定的SpanList中获取一个非空的span
  Span* GetOneSpan(SpanList& list, size_t alignBytes);
  // 哈希桶
  SpanList _spanLists[NFREELIST];
private:
  CentralCache()
  {}
  CentralCache(const CentralCache&) = delete;
  static CentralCache _sInst;
};


CentralCache.cpp:包含中心缓存类的定义。


#include "PageCache.h"
#include "CentralCache.h"
CentralCache CentralCache::_sInst;
// 从特定的SpanList中获取一个非空的Span
Span* CentralCache::GetOneSpan(SpanList& list, size_t alignBytes)
{
  assert(alignBytes >= 8);
  // 1、先到链表中找非空的Span
  Span* it = list.Begin();
  while (it != list.End())
  {
  if (it->_freeList != nullptr)
  {
    return it;
  }
  else
  {
    it = it->_next;
  }
  }
  // 2、走到这里说明没有list中没有非空的Span,那么就需要到PageCache申请特定页大小的Span
  // 这里可以释放桶锁,后面逻辑不再访问该桶
  list.GetSpanListMutex().unlock();
  // 申请PageCache的表锁,到PageCache中申请特定页的Span过来
  PageCache::GetInstance()->GetPageMutex().lock();
  Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(alignBytes));
  span->_isUse = true;
  PageCache::GetInstance()->GetPageMutex().unlock();
  // 把从PageCache申请到的Span切成一个个的小块定长内存
  char* start = (char*)(span->_pageId << PAGE_SHIFT);// 起始页地址
  size_t bytes = span->_n << PAGE_SHIFT;// 连续页的字节数
  char* end = start + bytes;// 最后一页的最后地址
  span->_freeList = start;
  start += alignBytes;
  void* tail = span->_freeList;
  while (start < end)
  {
  NextObj(tail) = start;
  tail = start;
  start += alignBytes;
  }
  NextObj(tail) = nullptr;
  // 3、重新申请桶锁,把切好的Span挂到桶上
  list.GetSpanListMutex().lock();
  list.PushFront(span);
  return span;
}
// 从中心缓存获取一定数量的小块定长内存给ThreadCache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t alignBytes)
{
  assert(batchNum > 0);
  assert(alignBytes >= 8);
  size_t index = SizeClass::Index(alignBytes);
  _spanLists[index].GetSpanListMutex().lock();
  // 从span中获取batchNum个小块定长内存
  // 如果不够batchNum个,有多少拿多少
  // 拿完后end存储的下一个节点要为置为空,span的自由链表也要对应更新
  Span* span = GetOneSpan(_spanLists[index], alignBytes);
  assert(span);
  assert(span->_freeList);
  start = span->_freeList;
  end = span->_freeList;
  size_t actualNum = 1;
  while (actualNum < batchNum && NextObj(end))
  {
  ++actualNum;
  end = NextObj(end);
  }
  span->_freeList = NextObj(end);
  NextObj(end) = nullptr;
  span->_useCount += actualNum;
  _spanLists[index].GetSpanListMutex().unlock();
  return actualNum;
}
// 回收自由链表中的所有小块定长内存到它们各自对应的Span中
void CentralCache::ReleaseListToSpans(void* start, size_t index)
{
  assert(start);
  assert(index >= 0);
  // 自由链表中的所有小块定长内存都来自于_spanLists[index]所存储的Span中
  _spanLists[index].GetSpanListMutex().lock();
  while (start)// 把每一个小块定长内存头插到对应Span的_freeList中
  {
  void* next = NextObj(start);
  Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
  NextObj(start) = span->_freeList;
  span->_freeList = start;
  --span->_useCount;
  // 如果发现回收一个之后Span的_useCount等于0说明Span切好分出去的所有小块定长内存都收回来了
  // 这时可以把这个Span回收到下一层PageCache中合并出更大页的Span
  if (span->_useCount == 0)
  {
    _spanLists[index].Erase(span);
    span->_freeList = nullptr;
    span->_next = nullptr;
    span->_prev = nullptr;
    //下面逻辑不再访问_spanList[index]桶的数据,所以可以暂时释放桶锁
    _spanLists[index].GetSpanListMutex().unlock();
    PageCache::GetInstance()->GetPageMutex().lock();
    PageCache::GetInstance()->ReleaseSpanToPageCache(span);
    PageCache::GetInstance()->GetPageMutex().unlock();
    _spanLists[index].GetSpanListMutex().lock();
  }
  start = next;
  }
  _spanLists[index].GetSpanListMutex().unlock();
}



4. PageCache相关文件


PageCache.h:包含页缓存的声明。


#pragma once
#include "Common.h"
// 饿汉的单例模式
class PageCache
{
public:
  // 返回PageCache的单例对象
  static PageCache* GetInstance()
  {
  return &_sInst;
  }
  // 返回给CentralCache一个k页的Span
  Span* NewSpan(size_t k);
  // 获取表锁
  std::mutex& GetPageMutex();
  // 传入小块定长内存获得所在页对应的Span对象
  Span* MapObjectToSpan(void* obj);
  // 对span前后的页,尝试进行合并,缓解外碎片问题
  void ReleaseSpanToPageCache(Span* span);
private:
  std::mutex _pageMtx;// 表锁
  SpanList _spanLists[NPAGES];// 存储未切分的span
  std::unordered_map<PAGE_ID, Span*> _idSpanMap;// 页号和span对象的映射
private:
  PageCache()
  {}
  PageCache(const PageCache&) = delete;
  static PageCache _sInst;
};


PageCache.cpp:包含页缓存的定义。


#include "PageCache.h"
PageCache PageCache::_sInst;
std::mutex& PageCache::GetPageMutex()
{
  return _pageMtx;
}
// 返回给CentralCache一个k页的Span
Span* PageCache::NewSpan(size_t k)
{
  assert(k > 0 && k < NPAGES);
  // 1、根据k直接定址映射,看对应SpanList桶中是否挂有Span
  if (!_spanLists[k].Empty())
  {
  Span* kSpan = _spanLists[k].PopFront();
  // 建立每一页的起始页号和kSpan的映射,方便CentralCache回收小块定长内存时,查找对应的Span
  for (size_t i = 0; i < kSpan->_n; ++i)
  {
    _idSpanMap[kSpan->_pageId + i] = kSpan;
  }
  return kSpan;
  }
  // 2、走到这里说明定址映射的桶中没有Span,那么看更大页的桶是否有Span
  for (size_t n = k + 1; n < NPAGES; ++n)
  {
  // 更大页的桶有Span的话就对其进行切分
  if (!_spanLists[n].Empty())
  {
    Span* kSpan = new Span();
    Span* nSpan = _spanLists[n].PopFront();
    kSpan->_n = k;
    kSpan->_pageId = nSpan->_pageId;
    nSpan->_n -= k;
    nSpan->_pageId += k;
    _spanLists[nSpan->_n].PushFront(nSpan);
    // 建立每一页的起始页号和kSpan的映射,方便CentralCache回收小块定长内存时,查找对应的Span
    for (size_t i = 0; i < kSpan->_n; ++i)
    {
    _idSpanMap[kSpan->_pageId + i] = kSpan;
    }
    return kSpan;
  }
  }
  // 3、走到这一步说明整个PageCache中找不到一个>=k页的Span,这时向堆申请一个128页的Span
  Span* bigSpan = new Span;
  void* ptr = SystemAlloc(NPAGES - 1);
  bigSpan->_n = NPAGES - 1;
  bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
  _spanLists[NPAGES - 1].PushFront(bigSpan);
  return NewSpan(k);
}
// 传入小块定长内存获得所在页对应的Span对象
Span* PageCache::MapObjectToSpan(void* obj)
{
  assert(obj);
  PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
  auto ret = _idSpanMap.find(id);
  if (ret != _idSpanMap.end())
  {
  return ret->second;
  }
  else
  {
  assert(false);
  return nullptr;
  }
}
// 对span前后的页,尝试进行合并,缓解外碎片问题
void PageCache::ReleaseSpanToPageCache(Span* span)
{
  // 向前合并
  while (1)
  {
  PAGE_ID prevId = span->_pageId - 1;
  auto ret = _idSpanMap.find(prevId);
  // 前面的页号没有,不合并了
  if (ret == _idSpanMap.end())
  {
    break;
  }
  // 前面相邻页的span在使用,不合并了
  Span* prevSpan = ret->second;
  if (prevSpan->_isUse == true)
  {
    break;
  }
  // 合并出超过128页的span没办法管理,不合并了
  if (prevSpan->_n + span->_n > NPAGES-1)
  {
    break;
  }
  // 合并操作只需调整Span的起始页页号和它的页数即可
  // 合并完成后把之前PageCache中挂的prevSpan移并delete
  span->_n += prevSpan->_n;
  span->_pageId = prevSpan->_pageId;
  _spanLists[prevSpan->_n].Erase(prevSpan);
  delete prevSpan;
  }
  // 向后合并
  while (1)
  {
  PAGE_ID nextId = span->_pageId + 1;
  auto ret = _idSpanMap.find(nextId);
  if (ret == _idSpanMap.end())
  {
    break;
  }
  Span* nextSpan = ret->second;
  if (nextSpan->_isUse == true)
  {
    break;
  }
  if (span->_n + nextSpan->_n > NPAGES - 1)
  {
    break;
  }
  span->_n += nextSpan->_n;
  _spanLists[nextSpan->_n].Erase(nextSpan);
  delete nextSpan;
  }
  // 前后都合并好了之后,把这个Span挂到PageCache对应页数的哈希桶中
  // 并处理好这个新Span首尾页的映射关系
  _spanLists[span->_n].PushFront(span);
  span->_isUse = false;
  _idSpanMap[span->_pageId] = span;
  _idSpanMap[span->_pageId + span->_n - 1] = span;
}



5. ConcurrentAlloc.h


该头文件中包含线程申请、释放小于128KB对象空间的接口。


#pragma once
#include "Common.h"
#include "ThreadCache.h"
static void* ConcurrentAlloc(size_t bytes)
{
  // 通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
  if (pTLSThreadCache == nullptr)
  {
  pTLSThreadCache = new ThreadCache;
  }
  return pTLSThreadCache->Allocate(bytes);
}
static void ConcurrentFree(void* ptr, size_t bytes)
{
  assert(pTLSThreadCache);
  pTLSThreadCache->Deallocate(ptr, bytes);
}


三. 回收内存联调


1. 单线程回收内存测试


我们在主函数中执行如下代码:


void TestSingleThreadFree()
{
  // 1、依次申请7个8bytes的小块定长内存
  void* p1 = ConcurrentAlloc(6);
  void* p2 = ConcurrentAlloc(8);
  void* p3 = ConcurrentAlloc(1);
  void* p4 = ConcurrentAlloc(7);
  void* p5 = ConcurrentAlloc(8);
  void* p6 = ConcurrentAlloc(3);
  void* p7 = ConcurrentAlloc(6);
  // 2、打印每个小块定长内存的起始地址
  cout << p1 << endl;
  cout << p2 << endl;
  cout << p3 << endl;
  cout << p4 << endl;
  cout << p5 << endl;
  cout << p6 << endl;
  cout << p7 << endl;
  // 3、依次释放7个8bytes的小块定长内存
  ConcurrentFree(p1, 6);
  ConcurrentFree(p2, 8);
  ConcurrentFree(p3, 1);
  ConcurrentFree(p4, 7);
  ConcurrentFree(p5, 8);
  ConcurrentFree(p6, 3);
  ConcurrentFree(p7, 6);
}



开始调试,申请7个小块8字节内存的过程直接跳过,观察输出结果应该是申请过程应该是成功了的:

e9639f91d2714e51b75eddafcf92c8ac.png


接下来进入第一个小块定长内存的释放逻辑,开始逐步调试:


10c14b74e25f4bafadbf6fda2ff53e43.png


首先是主线程调用自己的ThreadCache执行ThreadCache::Deallocate(...)函数去释放p1指向的小块定长内存:

bc86c95f1b314cd99fd4c313e77b1b33.png


PS:在进入ThreadCache::Deallocate(...)后不着急释放内存,先来观察线程的ThreadCache中自由链表桶的情况:


首先是主线程申请了7个8bytes的小块定长内存返回给外部,目前为止一个都还没有归还。

申请这7个小块内存的过程是:页缓存从128页的大Span中切分出一页的Span给中心缓存,然后中心缓存把这个一页的Span又切分成1024个8字节小块内存挂到它的_spanLists[0]这个桶上,通过慢启动反馈调节算法最后又将其中的10个小块内存拨给了ThreadCache,外部又从其中拿走了7个,那么ThreadCache的_freeLists[0]中就还剩3个小块内存,此时该自由链表桶的慢启动反馈调节值是5。


835207b0def64044b7c3cca1d915ad41.png


继续往后调试,p1、p2都被挂回到了ThreadCache的_freeLists[0]中,当p2挂到自由链表桶中时检测到桶里的小块内存个数等于该桶的慢启动反馈调节值5,这个时候应该调用ThreadCache::ListToolong(...)去回收ThreadCache::_freeLists[0]中的所有小块内存:


ea7c6a9ff06240ecb3d4990914a936b1.png

主逻辑进入到ThreadCache::ListToolong(...)中,该函数主要完成以下两个任务:


剥离ThreadCache::_freeLists[index]中的所有小块定长内存。

调用CentralCache::ReleaseListToSpans(...)让它去完成把剥离下来的小块内存归还到各自所属Span对象的工作。


aa98f171083f48c394625a5a02ab0958.png


接下来进入到中心缓存的ReleaseListToSpans(...)函数,通过PageCache::MapObjectToSpan(...)接口拿到之前被剥离的5个8bytes小块内存各自的Span对象,当前我们程序中回收的这5个小块内存都是来自同一个Span的,我们可以通过监视窗口看看未归还前这个Span对象的信息:

7cbdcb25a0c142f8a62538a6b276cd05.png


继续往下执行,把5个小块内存归还到这个一页大小的Span后,发现这个Span对象的_useCount减到了5,p2的释放到此为止:


31cfc88d3381484ea49248d0951b3609.png

接下来应该轮到p3的释放,此时主线程专属ThreadCache对象的_freeList[0]桶中没有挂任何一个小块内存,而且它的慢启动反馈调节值依然是5,继续运行,p3、p4、p5、p6这四个小块8bytes内存都要被头插到_freeList[0]这个桶中,然后结束,最后轮到释放p7时有所不同:


045fcd91840048d9884d8d588350d8e2.png

下面我们进入p7的释放逻辑,当p7头插到_freeList[0]中时,检测到自由链表中的所有小块内存又一次需要被归还到各自的Span对象中:


74b5628a34524a66bfba858a48a9dcd8.png

剥离完_freeList[0]中的小块内存后,又一次进入CentralCache::ReleaseListToSpans(…)去归还所以小块内存到各自的Span中:


5adf74ee3de0483bb907598ea31d20f0.png

这次把第5个小块内存归还到一页的Span后检测到这个Span对象的_useCount等于0了,说明它分出去给ThreadCache的所有小块8bytes内存全部收回来了,此时我们要把这个Span对象从中心缓存的_spanList[0]桶中剥离下来,然后把这个Span对象传入PageCache::ReleaseSpanToPageCache(...)到页缓存中去合并前后空闲的页:


0e2270c3c32c4753b884ba2d39834d2f.png

进入到PageCache::ReleaseSpanToPageCache(...)后,查找span对象前后页的id看它们是否空闲,空闲的话把包含它们的Span对象的所有页合并过来。由于传入进来的这个一页Span是之前页缓存中一个128页的大Span头删切分出去的,所以传入Span的后一页页号:span->_pageId + 1通过PageCache::_idSpanMap能查找到切分之后生成的另一个127页Span,二者合并最后能得到一个128页的大块Span。


82484253387d4102b61025d8c2849d0b.png

至此测试函数主体逻辑结束。


2. 多线程回收内存测试


在主线程中执行TestMultiThreadFree()函数:


void MultiThreadFree1()
{
  // 1、依次申请7个8bytes的小块定长内存
  std::vector<void*> v;
  for (size_t i = 0; i < 7; ++i)
  {
  void* ptr = ConcurrentAlloc(6);
  v.push_back(ptr);
  }
  // 2、依次释放7个8bytes的小块定长内存
  for (auto e : v)
  {
  ConcurrentFree(e, 6);
  }
}
void MultiThreadFree2()
{
  // 1、依次申请7个8bytes的小块定长内存
  std::vector<void*> v;
  for (size_t i = 0; i < 7; ++i)
  {
  void* ptr = ConcurrentAlloc(5);
  v.push_back(ptr);
  }
  // 2、依次释放7个8bytes的小块定长内存
  for (auto e : v)
  {
  ConcurrentFree(e, 5);
  }
}
int main()
{
  TestMultiThreadFree();
  return 0;
}



编译运行,没有崩溃,下面就不再对该测试函数做调试演示了:

6cc0c4e96cb74a38bc527f9170bb0ea6.png


相关文章
|
16天前
|
程序员 开发者
分代回收和手动内存管理相比有何优势
分代回收和手动内存管理相比有何优势
|
26天前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
|
1月前
|
算法 Java 程序员
内存回收
【10月更文挑战第9天】
44 5
|
1月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
1月前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
3月前
|
存储 NoSQL 算法
Redis内存回收
Redis 基于内存存储,性能卓越,但单节点内存不宜过大,以免影响持久化或主从同步。可通过配置 `maxmemory` 限制最大内存。内存达到上限时,Redis采用两种策略:内存过期策略和内存淘汰策略。过期策略包括惰性删除和周期删除,后者分为 SLOW 和 FAST 模式。内存淘汰策略有八种,如 LRU、LFU 和随机淘汰等,用于在内存不足时释放空间。官方推荐使用 LFU 算法。
Redis内存回收
|
3月前
|
JavaScript 前端开发 算法
js 内存回收机制
【8月更文挑战第23天】js 内存回收机制
39 3
|
3月前
|
存储 缓存 NoSQL
Redis内存管理揭秘:掌握淘汰策略,让你的数据库在高并发下也能游刃有余,守护业务稳定运行!
【8月更文挑战第22天】Redis的内存淘汰策略管理内存使用,防止溢出。主要包括:noeviction(拒绝新写入)、LRU/LFU(淘汰最少使用/最不常用数据)、RANDOM(随机淘汰)及TTL(淘汰接近过期数据)。策略选择需依据应用场景、数据特性和性能需求。可通过Redis命令行工具或配置文件进行设置。
83 2
|
2月前
|
数据安全/隐私保护 虚拟化
基于DAMON的内存能回收 【ChatGPT】
基于DAMON的内存能回收 【ChatGPT】
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
381 0