TCMalloc为每个Thread预分配一块缓存.每个Thread在申请内存时会首先从这个缓冲区ThreadCachae申请.并且所有ThreadCache缓存区还共享一个叫做CentralCache的中心缓存.假设Go语言内存管理用的是原生TCMalloc模式.线程与内存的关系如图所示.
这样做的好处其一是ThreadCache作为每个线程独立的缓存.能够明显的提高Thread获取高命中的数据.其二是ThreadCache从堆空间一次性申请.即只触发一次系统调用.每个ThreadCache还会共同访问CentralCache.CentralCache是所有线程共享的缓存.当ThreadCache的缓存不足时.就会从CentralCache获取.当ThreadCache缓存充足或过多时.则会将内存退还给CentralCache.但是CentralCache由于共享.所以访问一定要加锁.ThreadCache作为线程独立的第一交互内存.访问无须加锁.CentralCache则作为ThreadCache临时补充缓存.
TCMalloc的内存分类如下图.
为了解决中对象和大对象的内存申请.TCMalloc依然有一个全局共享内存堆PageHeap.如图所示.
PageHeap也是通过一次系统调用从虚拟内存中申请的.PageHeap很明显是全局的.所以访问时一定要加锁.作用是当CentralCache没有足够内存时会从PageHeap获取.当CentralCache内存过多或者充足时.则将低命中内存块退还PageHeap.如果Thread需要大对象申请超过Cache容纳的内存块单元.则会直接从PageHeap获取.
TCMalloc模型结构:
1).Page:
TCMalloc中将虚拟内存空间划分为多份同等大小的Page.每个Page默认为8KB.对于TCMalloc来讲.虚拟内存空间的全部内存都按照Page的容量被分成均等份.并且给每份Page标记了ID编号.如图所示.
对Page进行编号的好处是.可以根据任意内存的地址指针.进行固定算法偏移计算.以此计算出所在的Page.
2).Span:
多个连续的Page称为是一个Span.其含义与操作系统管理的页表相似.Page与Span的关系如图所示.
TCMalloc以Span为单位向操作系统申请内存.每个Span记录了第一个起始Page的编号Start和一共有多少个连续Page的数量Length.为了方便Span和Span之间的管理.Span集合以双向链表的形式构建.如图所示.
3).Size Class:
对于256KB以内的小对象.TCMalloc会将这些小对象集合划分成多个内存刻度.同属于一个刻度类别下的内存集合称为一个Size Class.
每个Size Class都对应一个大小.例如8字节 16字节 32字节等.在申请小对象内存的时候.TCMalloc会根据使用方申请的空间大小就近向上取最接近的一个Size Class的Span(由多个等空间的Page组成)内存块返给使用方.
ThreadCache:
在TCMalloc中每个线程都会有一份单独的缓存.即ThreadCache.ThreadCache中对于每个Size Class都会有一个对应的FreeList.FreeList表示当前缓存中还有多少空闲的内存可用.具体的结构如图所示.
使用方对于从TCMalloc申请的小对象.会直接从ThreadCache获取.实则是从FreeList中返回一个空闲对象.如果对应的Size Class刻度下已经没有空闲的Span可以被获取.则ThreadCache会从CentralCache中获取.当使用方使用完内存后,归还时也是直接归还给当前的ThreadCache中对应刻度下的FreeList.
整条申请和归还的流程不需要加锁.因为ThreadCache为当前线程独享.如果ThreadCache不够用.则需要从CentralCache申请内存.这个动作需要加锁,不同Thread之间的ThreadCache是以双向链表的结构进行关联.这是为了方便TCMalloc进行统计和管理.
CentralCache:
CentralCache由各个线程共用.所以与CentralCache获取内存交互时需要加锁.CentralCache缓存的Size Class和ThreadCache的Size Class一样.这些缓存都被放在CentralFreeList中,当ThreadCache中的某个Size Class刻度下的缓存小对象不够用时.就会向CentralCache对应的Size Class刻度的CentralFreeList获取.同样的.如果ThreadCache有多余的缓存对象.则会退还给响应的CentralFreeList.如图所示.
Central与PageHeap的角色关系与ThreadCache与CentralCache的角色关系相似.当CentralCache出现Span不足时.会从PageHeap申请Span.以及将不在使用的Span退还给PageHeap.
PageHeap:
PageHeap是提供CentralCache的内存来源.PageHeap与CentralCache不同的是CentralCache是与ThreadCache布局一模一样的缓存.主要针对ThreadCache的一层二级缓存起作用.并且只支持小对象内存分配.而PageHeap则是针对CentralCache的三级缓存.弥补对于中对象和大对象内存的分配.PageHeap是直接和操作系统虚拟内存衔接的一层缓存.当ThreadCache CentralCache PageHeap都找不到合适的Span时.PageHeap则会调用操作系统的内存申请系统的调用函数来从虚拟内存的堆区中取出内存填充到PageHeap中.如图所示.
PageHeap内部的Span管理.采用两种不同的方式.对于128个Page以内的Span申请.每个Page刻度都会用一个链表形式的缓存来存储.对于128个Page以上的内存申请.PageHeap以有序集合来存放.
TCMalloc的小对象分配:
1).Thread用户线程应用逻辑申请内存.当前Thread访问对应的ThreadCache获取内存.此过程不需要加锁.
2).ThreadClass得到申请内存的SizeClass(一般向上取整.大于或等于申请的内存大小.).通过SizeClass索引去请求自身对应的FreeList.
3).判断得到的FreeList是否为非空.
4).如果FreeList为非空.则表示目前有对应内存空间供Thread使用.得到FreeList第一个空闲Span后返给Thread用户逻辑.流程结束.
5).如果FreeList为空.则表示目前没有对应的SizeClass的空闲Span可使用.请求CentralCache并告知Central具体的SizeClass.
6).CentralCache收到请求后.加锁访问CentralFreeList.根据SizeClass进行索引找到对应的CentralFreeList.
7).判断得到的CentralFreeList是否为非空.
8).如果CentralFreeLIst为非空.则表示目前有空闲的Span可使用.返回多个Span.将这些Span(除了第一个Span)放置于ThreadCache的FreeList中.并且将第一个Span返给Thread用户逻辑.流程结束.
9).如果CentralFreeList为空.则表示目前没有可用的Span.向PageHeap申请对应大小的Span.
10).PageHeap得到CentralCache的申请.加锁请求对应的Page刻度的Span链表.
11).PageHeap将得到的Span根据本次流程请求的SizeClass大小为刻度进行拆分.分成N份SizeClass大小的Span返给CentralCache.如果有多余的Span.则返回PageHeap对应Page的Span链表中.
12).CentralCache得到对应的N个Span.添加至CentralFreeList中.跳转至第8步.
TCMalloc中对象分配:
中对象为大于256KB且小于或等于1MB的内存.对于中对象申请内存分配的刘承宇小对象分配有一定区别.如下图所示.
1).Thread用户逻辑层提交内存申请.如果本次申请超过256KB但不超过1MB 则属于中对象申请.TCMalloc直接向PageHeap发起申请Span请求.
2).PageHeap接收到申请后需要判断本次申请是否属于小Span(128个Page以内).如果是则申请小Span.即中对象申请流程.如果不是.则进入大对象申请流程.
3).PageHeap根据申请的Span在小Span的链表中间向上取整.得到最适合的第K个Page刻度的Span链表.
4).得到第K个Page链表刻度后.将K作为起始点.向下遍历找到第一个非空链表.直至128个Page刻度位置.如果找到.则停止.将停止处的非空Span链表作为提供此次返回的内存Span.将链表中的第一个Span取出.如果找不到非空链表.则将本次申请当作大Span申请.进入大对象申请流程.
5).假设本次获取的Span由N个Page组成.PageHeap将N个Page的Span拆分成两个Span.其中一个为K个Page组成的Span.作为本次内存申请的返回.返给Thread.另一个为N-K个Page组成的Span/重新插入N-K个Page对应的Span链表中.
TCMalloc大对象分配:
1).Thread用户逻辑层提交内存申请.如果本次申请内存超过了1MB.则属于大对象申请.TCMalloc将直接向PageHeap发起申请Span.
2).PageHeap接收到申请后需要判断本次申请是否属于小Span(128个Page以内).如果是.则进入小Span中对象申请流程.如果不是.则进入大对象申请流程.
3).PageHeap根据Span的大小按照Page单元进行除法运算.向上取整.得到最接近的Span的且大于Span的Page倍数K.此时的K应该大于128.如果是从中对象流程分过来的(中对象申请流程可能没有非空链表提供Span).则K值应该小于128.
4).搜索Large Span Set集合.找不到小于K个Page的最小Span(N个Page).如果没有找到合适的Span.则说明PageHeap已经无法满足需求.遇到此种情况时向操作系统虚拟内存的堆空间申请一些堆内存.将申请到的内存安置在PageHeap的内存结构中.重新执行步骤3.
5).将从Large Span Set集合得到的N个Page组成的Span拆分成两个Span.K个Page的Span直接返给Thread用户逻辑.N-K个Span退还给PageHeap.其中如果N-K大于128.则退还到Large Span Set集合中.如果N-K小于128.则退还到Page链表中.
年年往事空回首.年年情深如旧.
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路