内存池的面试整理

文章思路来源

  • 如何实现无锁申请?
    • 每个线程申请自己的TreadCacheTLS对象,来管理自己的freeList数组。
  • 小内存的大小?
    • 0-256K,并且对申请到的内存进行字节对齐,保证申请到的内存可以映射到对应的freeList中。
    • 映射规则?
      • 从128字节开始每个区间8倍递增,从16(8到16是两倍)字节开始对齐每8倍递增,到256KB,8K对齐结束。每个区间为16,56,56,56,24,共208个freeList
    • 为什么从8字节开始对齐?让freeList保存下一个节点的地址,即使在64位系统下
  • freeList如何管理内存?
    • 将申请到的内存块头插到对应的freeList,每次需要内存就头删一个内存块。
  • 如何向centralCache申请内存?
    • 首先利用相同的哈希值来得到对应的spanList,获取到一个span,里面存储的是切好size大小的freeList,并且物理空间是连续的。然后返回ThreadCache需要的内存块数。
    • 如何连续?
      • 将申请到的未切分span,进行尾插
  • 如何向pageCache申请内存?
    • 首先将centralCache申请的页数(不足一页申请一页),映射到对应的spanList上,取出一个span,并记录span内每个页号和span的映射关系后,返回给centralCache。
    • 为空则向spanList后遍历,如果找到一个span,则再创建一个span,一个span存所需的页,返回给centralCache,一个span存剩余的页,并将这个span挂到对应spanList内。
    • 都为空,则向系统申请一个128页的大span,重新进行前面的流程。创建两个span,一个span返回,一个span挂到对应spanList内。
  • 为什么双向链表?
    • 方便回收span时,找到前一个节点
  • 项目中有使用new吗
    • 新建了一个定长内存池,每次需要对象时从这里面申请,并且用的是定位new,直接再给定地址内开辟空间
  • 内存池只分配内存给对象,至于内存如何使用就看对象如何使用比如将void*转换为int*然后修改指向的空间
  • 什么时候加锁?
    • centralCache从span中返回内存块时加锁(删除)
    • centralCache将pageCache中的span挂到spanList时加锁(增加)
    • centralCache合并内存块到span需要加锁(合并)
    • PageCache寻找可用span时需要加锁(删除)
    • PageCache合并span时需要加锁(合并)
    • 一般涉及到对数据的增删改操作时,需要加锁
  • 如何释放内存?
    • ThreadCache释放指向内存空间的指针时,会根据指针地址找到页号然后找到对应的span,进而知道内存块大小,就能找到归还的freeList,当归还的内存块等于批量申请的内存块数量后,就可以归还这个freeList给span
    • 调用centralCache函数,找到该freeList的所属的spanList,加锁后,根据页号和span的关系,将freeList归还给spanList内的span,解锁后
    • 调用PageCache函数,PageCache同样利用页号和span的关系进行向前向后合并,将合并后的span挂到对应spanList内,并在spanList内删掉合并用掉的span
    • 页号和地址的可以互相转换,只需要将地址除8K得到页号,因为每个地址都是以8K递增,相应的页号+1
    • 向前向后合并的条件是,span不为空且没有被使用
  • 大内存如何申请?
    • 计算对齐后需要的页数,然后PageCache向系统申请空间,建立页号和span的映射后返回该span,利用span的页号左移13位得到指针地址并返回。释放该内存,则直接释放给系统,而不是还给内存池。
  • 链表的头节点,如何连接?
    • 头几个字节,并且为了适配不同平台,将一级指针强转为二级指针再取其地址。
  • 改进的点?
    • 使用RAII机制管理锁
    • 加入日志线程
  • freeList,spanList
    • freeList记录申请内存块数,批量申请的数量
    • spanList记录span的头节点
    • span记录页号、页数、前后指针、切好的size大小、使用掉的内存块数、是否在使用
  • 慢开始机制
    • 申请块数以1递增,直到当前申请的内存块数大于最大可申请数
  • 亮点/细节:
    • 慢开始机制
    • 基数树
    • 为了保证连续,进行尾插
    • 使用定长内存池模板,每次创建list的头节点都从里面申请

  • ConcurrentAlloc:首先,当线程需要size个字节时,会从定长内存池中先加锁创建tls线程缓存对象,
  • Allocate:将size进行字节对齐,并计算key找到对应的freeList,从该freeList中无锁的获取头部的内存块(哈希桶用每个定长内存块的头部来作为链表下一个节点的地址)
    (哈希桶的映射规则是
    0-128,以8字节对齐,分为0-15,
    129-1K,以16字节对齐,分为16-71,
    1K+1-8K,以128字节对齐,分为72-127,
    8K+1-64K,以1024字节对齐,分为128-183,
    64K+1-256K,以8K对齐,分为184-207)
  • FetchFromCentralCache:如果对应freeList为空,则向中间缓存利用慢开始机制申请一定数量的内存块,
  • FetchRangeObj:首先中间缓存根据相同的key来找对应的spanList,内部存储切好size大小的freeList(每个span(大小为8K)内包含一定数量的size内存块,)然后加锁从桶内的一个span获取(头删)需要的内存块数,线程缓存得到中间缓存分配的内存块后,将第一个内存块分给线程,将剩余的内存块头插到freeList里。
  • GetOneSpan:如果中间缓存的当前span哈希桶为空,首先把中间缓存锁解锁用以防止其他线程释放内存时需要等待,然后加上页锁,向页缓存申请空间,
  • NewSpan:页缓存保存的是管理页的spanList,它的映射规则是直接定址法,每个key保存的都是key个页的span。
    • 中间缓存需要申请k个页时映射到对应spanList上,取出一个span,建立页号和span的映射,返回给中间缓存,
    • 如果对应spanList没有span,就遍历k之后的spanList,有则切分k个,并将剩余的span头插到对应的spanList上,建立页号和span的映射,返回这个span。
    • 如果k之后的spanList都为空,则从定长内存池新建一个span对象,为其分配128页连续的内存空间,然后重新切分k,头插剩余的span到对应spanList内+映射,返回这个span。
  • GetOneSpan:中间缓存拿到span后解锁页缓存锁,将span划分为size大小的内存块,保持物理空间连续,将切好的span加锁后头插到对应spanList内,从span内获取n个对象返回给线程缓存,线程缓存在返回一个对象给线程,并把剩下的对象头插到对应freeList内。

释放内存:

MapObjectToSpan:计算指针地址到span的映射,获取内存块的大小
Deallocate:根据内存块的大小归还到相应freeList内,当其内的内存块数量大于分配的数量后,
分配的数量有慢开始机制计算,每次申请就递加一个,(机制)
ListTooLong:就把这些归还给centralCache
ReleaseListToSpans:根据相同的key找到spanList后,加锁后,根据映射关系将内存块归还给span里的freeList,直到所有内存块归还完毕后就将这个span归还给pageCache,解锁
ReleaseSpanToPageCache:加锁,首先看

  • 向前能否合并,根据前一个span的页号找到前一个span判断是否为空,是否正在被使用,是则无法合并
    • 然后向前合并页号以及页数,并把前一个span对应的spanList删除,并且回收前一个span的空间
    • 循环上述判断直到退出
  • 向后能否合并,根据后一个span的页号(span+页数=后一个)找到对应span,进行向后合并,直到不满足后一个span不为空,未使用。
  • 将合并后的span挂到指定位置,并建立页号和span的映射关系(重复则覆盖)
  • 解锁

你可能感兴趣的:(面试,职场和发展)