文章思路来源
- 如何实现无锁申请?
- 每个线程申请自己的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的映射关系(重复则覆盖)
- 解锁