1.thread cache是内存池中的第一层缓存,
这一层缓存主要解决的问题就是并发状态下锁竞争的效率问题。
线程在这里申请不需要加锁,每一个线程都有自己独立的cache,这也就是这个项目并发高效的地方。
怎么实现每个线程都拥有自己唯一的线程缓存呢?
为了避免加锁带来的效率,在thread cache中使用(tls)thread local storage保存每个线程本地的thread cache的指针,这样thread cache在申请释放内存是不需要锁的。
因为每一个线程都拥有了自己唯一的一个全局变量。
tls分为静态的和动态的:
静态的tls是:直接定义
动态的tls是:调用系统的api去创建的,我们这个项目里面用到的就是静态的tls
2.
从thread cache申请内存:
当内存申请size<=64k时在thread cache中申请内存,
计算size在自由链表中的位置,
如果自由链表中有内存对象时,
直接从freelist[i]中pop一下对象,
时间复杂度是o(1),且没有锁竞争。
当freelist[i]中没有对象时,
则批量从central cache中获取一定数量的对象,
插入到自由链表并返回一个对象。
向thread cache释放内存:
当释放内存小于64k时将内存释放回thread cache,
计算size在自由链表中的位置,将对象push到freelist[i].
当链表的长度过长,
也就是超过一次最大限制数目时则回收一部分内存对象到central cache。
1.central cache是内存池中的第二层缓存,
这一层缓存主要解决的问题就是内存分配不均的问题。
在设计thread cache时,我们利用tls让每一个线程都独享了一个thread cache,但是这样虽然解决了内存碎片的问题,也导致了另一个问题,那就是内存资源分配不均衡的问题。当一个线程大量的开辟内存再释放的时候,这个线程中的thread cache势必会储存着大量的空闲内存资源,而这些资源是无法被其他线程所使用的,当其他的线程需要使用内存资源时,就可能没有更多的内存资源可以使用,这也就导致了其它线程的饥饿问题。为了解决这个问题,就有了central cache的设计。
central cache的主要功能:周期性的回收thread cache中的内存资源,避免一个或多个线程占用大量内存资源,而其它线程内存资源不足的问题。让内存资源的分配在多个线程中更均衡。
2.第二层是central cache,在这里是所有线程共享的,,central cache是存在竞争的,所以在这里取内存对象的时候是需要加锁的,但是锁的力度可以控制得很小。
为了保证全局只有唯一的central cache,这个类被可以设计成了单例模式
单例模式采用饿汉模式,避免高并发下资源的竞争
3.central cache起着承上启下的作用
对下,它既可以将内存对象分配给thread cache来的每个线程,又可以将线程归还回来的内存进行管理。
对上 它要把自身已经满了的内存块上交给页缓存 当自己有需要的时候 得向页缓存申请
4.central cache本质是由一个哈希映射的span对象自由双向链表构成 一个span对象大小是恒定的4k大小(32位下4k 64位下8k ) 但是中心缓存数组每个元素指定了单个span划分成内存块的大小 (比如第一个8bytes 第二个16bytes等等),故他们能挂载的内存块数不一样
5.central cache本质是由一个哈希映射的span对象自由双向链表构成 一个span对象大小是恒定的4k大小(32位下4k 64位下8k ) 但是中心缓存数组每个元素指定了单个span划分成内存块的大小 (比如第一个8bytes 第二个16bytes等等),故他们能挂载的内存块数不一样
6.从central cache申请内存:
当thread cache中无法满足线程提出的内存申请的时候,就会批量向central cache申请一些内存对象比如线程申请一个16bytes的内存 但是此时thread cache 中16bytes往上的都没了 ,这个时候向cantral申请 central cache就到16bytes的地方拿下一个span 给thread cache,这个过程是需要加锁的。
当左边步骤中,向central cache中申请发现16bytes往后的span结点全空了时,则将空的span链在一起,然后向page cache申请若干以页为单位的span对象,比如一个3页的span对象,然后把这个3页的span对象切成3个一页的span对象 放在central cache中的16bytes位置可以有三个结点,再把这三个一页的span对象切成需要的内存块大小 这里就是16bytes ,并链接起来,挂到span中。
6.向central cache中释放内存
释放内存:
当thread cache过长或者线程销毁,则会将内存释放回central cache中的,
比如thread cache中16bytes部分链表数目已经超出最大限制了 则会把后面再多出来的内存块放到central cache的16bytes部分的他所归属的那个span对象上.
此时那个span对象的usecount就减一位
当_usecount减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并。
注意由span对象划分出去的内存块和这个span对象是有归属关系的
所以由thread cache归还释放某个内存(比如16bytes)应该归还到central cache的16bytes部分的他所归属的那个span对象上
怎么才能将thread cache中的内存对象还给它原来的span呢?
答:可以在page cache中维护一个页号到span的映射,当page cache给central cache分配一个span时,将这个映射更新到unordered_map中去,这样的话在central cache中的span对象下的内存块都是属于某个页的 也就有他的页号,,同一个span切出来的内存块pageid都和span的pageid相同,这样就能很好的找出某个内存块属于哪一个span了。也就能通过unorder_map映射到span的id上,从而找到span,在thread cache还给central cache时,可以查这个unordered_map找到对应的span。
0.page cache的主要功能就是回收central cache中空闲的span,并且对空闲的span进行合并,合并成更大的span,当需要更大内存时,就不需要担心因为内存被切小而无法使用大内存的情况了,缓解了内存碎片的问题。而当central cache中没有可用的span对象时,page cache也会将大的内存切成需要大小的内存对象分配给central cache。
page cache中的内存是以页为单位储存和分配的。
1.存储的是以页为单位存储及分配的(中心缓存数组每个元素表示链表中span的页数),
中心缓存没有span时(所有的span链表都空了),从页缓存分配出一定数量的页,并切割成定长大小的小块内存(在中心缓存中对应的字节数 ),分配给central cache。page cache会回收central cache满足条件的span(使用计数为0,即span结点满足一页)对象,并且合并相邻的页(根据页id相邻),组成更大的页,缓解内存碎片的问题。
可以理解为central cache中的span和page cache中是不一样的
central cache中的span只代表一页 然后被分解成了小的内存块链接而成的链表
page cache中的span是有多页组成分的
2.page cache是一个以页为单位的span自由链表。
为了保证全局只有唯一的page cache,这个类可以被设计成了单例模式。
本单例模式采用饿汉模式。
3.向page cache申请内存
当central cache向page cache申请内存时,
当thread cache向central cache中申请,发现central cache16bytes往后的span结点全空了时,则将空的span链在一起,然后向page cache申请若干以页为单位的span对象,比如一个3页的span对象,page cache先检查3页的那个位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是3page,3page后面没有挂span,则向后面寻找更大的span,假设在10page位置找到一个span,则将10page span分裂为一个3page span和一个7page span。
如果找到128 page都没有合适的span,则向系统使用mmap、brk或者是virtualalloc等方式申请128page span挂在自由链表中,再重复1中的过程。
然后把这个3页的span对象切成3个一页的span对象 放在central cache中的16bytes位置可以有三个结点,再把这三个一页的span对象切成需要的内存块大小 这里就是16bytes ,并链接起来,挂到span中。
4.向page cache中释放内存
当thread cache过长或者线程销毁,则会将内存释放回central cache中的,
比如thread cache中16bytes部分链表数目已经超出最大限制了 则会把后面再多出来的内存块放到central cache的16bytes部分的他所归属的那个span对象上.
此时那个span对象的usecount就减一位
当_usecount减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会依次寻找span的前后相邻pageid的span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
5.page cache中最重要的就是合并了,在span结构中,有_pageid(页号)和_pagequantity(页的数量)两个成员变量,通过这两个成员变量,同时再利用unordered_map建立每一个页号到对应span的映射,就可以通过页号找到对应span进行合并了。
那么页号又是如何来的呢?当我们通过virtualalloc(windows环境下是virtualalloc,linux下使用brk或者mmap)直接向系统申请内存时,都是以页为单位的内存,而在32位机器下,一页就是4k。所以从0开始每一页的起始地址都是4k的整数倍,那么只需要将申请到的内存地址左移12位就可以得到相应的页号了,而通过页号也可以计算每一页的起始地址,只需要将地址右移12位即可。
具体合并的过程是这样的:假如现在有一个pageid为50的3页的span,有一个pageid为53的6页的span。这两个span就可以合并,会合并成一个pageid为50的9页的span,然后挂在9页的spanlist上。