内存碎片问题;
性能问题;
高并发(在多线程同时申请内存时,效率依旧很高)。
threadcache是解决高并发的性能问题,因为没有锁;
centralcache是平衡threadcache资源竞争的问题,避免一个线程用多了,另一个线程没得用;但是这里的平衡会付出一定的代价,当多个线程同时来申请时需要加锁,CentralCache下面挂的是一个一个的span,span是一些内存对象的集合,这些内存是以页为单位,每个span下面又挂了不同字节的对象;
PageCache的span是这个对象没有切开的,它下面挂的是1页的内存,它的里面的_list为空,对象还没有被切开,它还用不上切开的对象,它主要是做页的分割和合并,解决内存碎片问题的,它连页的起始地址都没有,不过他有页号和它是几页的,这样就可以它的起始地址和内存数。
关于TLS
内存池这个项目是基于三级缓存结构的内存池,它在一定程度上减轻了malloc遗留下来的的内存碎片问题;还实现了多线程并发申请内存时,效率依旧很高。对于内存碎片,整体控制在12%左右;为了达到高并发时仍旧高效,使用线程本地存储(TLS),达到每个线程并发申请时,互不干扰。
TLS(Thread Local Storage)线程局部存储,保存每个线程本地的ThreadCache的指针,作用是申请和释放内存不用加锁。TLS变量里面:每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。
TLS原理剖析:(定义一个全局变量,这个全局变量只属于当前线程,其它线程看不见)为了找到每一个ThreadCache,我们可以保存ThreadCache的指针,并定义一个全局变量,但是全局变量只能指向其中一个,这样的话其它的ThreadCache就找不到了,所以这时候,我们把用来保存不同ThreadCache的指针链接起来,再定义一个全局指针指向它(其中一个threadcache),这样的话,所有的线程找到的都是这个全局指针,然后再通过全局指针找到属于自己的threadcache,每个线程里面都有一个pid,就可以确认是属于哪个线程的。
我们把:定义一个全局变量,但是这个全局变量不是所谓的所有线程共享,而是每个线程自己的,相当于每个线程定义了自己的全局变量,每个线程都有一个全局变量,这样当有多个线程同时来并发申请内存时,就可以自己找自己的ThreadCache,不受到别人的干扰,系统有一种机制,定义这样的变量,就叫TLS。
静态的TLS是:直接定义
动态的TLS是:调用系统的API去创建的,我们这个项目里面用到的就是静态的TLS。
三层结构剖析
ThreadCache
第一次来,每个线程都要调内存申请的这个接口,第一次调的时候,threadcache的指针为空,这个时候就会创建一个threadcache的一个对象,每个线程是如何获取自己的threadcache呢?为了避免加锁,使用TLS做到不用加锁(稍微解释一下TLS,避免和网络中的TLS库混淆),threadcache是一个挂了对象的自由链表,申请的时候,只要是8字节到64k的内存大小,都来ThreadCache这里申请,申请的时候,有一套它的映射规则:一开始是128字节以内,以8字节为一个间隔,走它的映射,129-1024是以16字节为间隔;控制在%12左右的内碎片的浪费,而且这个间隔为了好算,取的是2的次方倍,整体把内碎片浪费率控制在%12左右:
整体把内碎片浪费率控制在%12左右
[1,128] 8byte对齐 freelist[0,16) 128/8=16
[129,1024] 16byte对齐 freelist[16,72) (1024-129)/16=56
[1025,8*1024] 128byte对齐 freelist[72,128) (8*1024-1025)/128=56
[8*1024+1,64*1024] 1024byte对齐 freelist[72,184)
CentralCache
CentralCache的映射规则和threadcache的映射规则一模一样,CentralCache处于一个承上启下的作用,对上平衡threadcache的资源竞争问题,对pagecache是:辅助页缓存进行页的合并,当一个span的对象全部释放回来时,将span还给pagecache,并且进行页合并。
PageCache
PageCache主要是做页的分割和合并,有页号和是几页的,就可以算它的起始地址和内存数了。PageCache下面挂的也是一个一个的span,但是CentralCache和PageCache的span是不一样的,首先映射规则不一样,CentralCache和ThreadCache一样,而PageCache是以页为单位的映射规则:1页,2页,一直到128页。而CentralCache是以8字节为间隔的(控制住内存的浪费率在不超过%12)。
高并发如何实现(即申请的过程)
thread cache
申请内存的过程
线程要申请内存不是直接去申请的,是被动去申请的,当一个线程来申请内存空间时,给它创建一个thread cache,同时调用并发申请接口去申请内存。(TLS保证了每个线程都有一个自己的thread cache)
当线程申请的内存小于等于64kb时,就直接在thread cache里对应位置的链表处申请,时间复杂度O(1),并且没有锁竞争。如果此时对应位置的链表上并没有挂内存块的话,就会去central cache里去申请一块批量的内存,然后返回一块空间,并将剩下的内存块挂在自己的thread cache里的对应FreeList里。
多线程并发的去central cache里获取批量内存时central cache要加锁,并且central cache是一个单例模式,全局只有一个,虽然这里会加锁导致线程串行去申请批量的内存,但是每个线程每次来申请的内存是批量的。假如一次申请的批量内存是50个,那么之后的49次都不需要再去找central cache要了。
central cache
中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache周期性的回收thread cache中的对象,避免一个线程占用了太多的内存。达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁的,并且central cache在全局只有一个,因此要设计为单例模式,不过一般情况下在这里取内存对象的效率非常高,并且一次给thread cache的内存是批量的,所以这里竞争不会很激烈。
申请内存的过程
当thread cache里对应FreeList链表里没有挂内存块时,就会来central cache里拿内存,SpanList也是一个哈希映射的链表,根据要申请内存块的大小找到对应的SpanList,在SpanList中找一个不为空的span,然后将这个span中提前切好的内存块批量的给thread cache。如果对应的SpanList里没有一个不为空的span时,就会去page cache里去申请一个span对象,span对象里是以页为单位的内存,将这个span中的内存切成对应的大小,然后将这个span对象挂在这个SpanList里。
page cache
申请内存的过程
当central cache的SpanList里没有span或者没有不为空的span时,central cache就会向page cache里申请一个span。首先会去看对应页数的SpanList里有没有挂span,如果有直接返回一个对应页数的span,如果没有就会向后遍历,寻找较大页数的span进行切分。如果找到128页的位置都没有找到一个span时,就会直接去系统申请一块128页的内存,然后进行切分。
具体的切分过程是这样的:假如我已经向系统申请了一块128页的内存然后挂在128页的SpanList上,如果我需要1页的span,就会将这个128页的span切分成一个1页的span和一个127页的span,再将这个1页的span切分好返回给central cache,然后把剩下的127页的span挂在127页的SpanList上。
内存碎片是如何处理的(即释放的过程)
thread cache
释放内存的过程
当某个线程用完一块内存要归还给它自己的thread cache时,直接根据内存块的大小找到对应的位置插入到对应位置的FreeList链表中。如果链表的长度过长,就要回收一部分内存块到central cache里,保证每一个FreeList里不会挂太多的内存块。
central cache
释放内存的过程
当给用户的内存用完时,就会将内存挂到threadcache中的对应的FreeList中,当对应的thread cache里某个FreeList里挂的内存太多时,就会批量的释放回central cache,这时中心缓存中span里的_usecount会减去归还的内存块的个数,当span中的_usecount等于0时,就说明所有内存已经全部归还。所以在SpanList里不会存在_usecount等于0的span,因为一旦某个span的_usecount为0时,就会被移出这个SpanList,然后去page cache里进行span的合并。
需要注意的是:
这里归还回来的内存块极有可能来自不同的span,所以这里用到了STL中的关联容器map,建立了PageID和span的映射,同一个span切出来的内存块PageID都和span的PageID相同,这样就能很好的找出某个内存块属于哪一个span了。
page cache
释放内存的过程
如果central cache的某个span的_usecount为0时,该span就会被释放回page cache里,此时page cache有个空闲的span,这个span会依次寻找它的前后页的span,看是否可以合并,如果可以合并,就会继续向前寻找。这样就可以将切小的内存合并成大的span,减少内存碎片。具体合并的过程是这样的:假如现在有一个PageID为50的3页的span,有一个PageID为53的6页的span。这两个span就可以合并,会合并成一个PageID为50的9页的span,然后挂在9页的SpanList上。
三级缓存结构
ThreadCache:线程缓存整体是一个对象数组,数组的每一层是一个自由链表。
CentralCache:中心缓存是一个基于单例模式的跨度数组,数组的每一层是一个双向带头循环的跨度链表,它的作用是平衡多线程的资源竞争问题。
PageCache:页缓存也是一个基于单例模式的结构,主要作用是完成大批量内存的申请、释放和页的合并(内存碎片问题)。
三层结构的整体框架图如下: