TCMalloc
TCMalloc(Thread-Caching Malloc)是由Google开发的一种内存分配器,用于替代标准C库中的malloc和free函数。它主要用于提高多线程环境下程序的内存分配和释放性能,特别是在高并发情况下。使用TCMalloc能够有效地提升程序的性能,特别是在多线程并发环境下,减少锁竞争和内存碎片对性能造成的影响。
- 多线程优化:TCMalloc的一个主要设计目标是优化多线程环境下的内存分配效率。它采用了一种线程本地缓存(Thread-Caching)的策略,将内存分配请求按线程ID缓存在本地,减少了多线程下的锁竞争,提高了分配和释放内存的性能。
- 优化内存碎片:TCMalloc会尽量减少内存碎片的产生,通过一些优化策略如对象池(Object Pool)和内存合并等,降低内存碎片对程序性能的影响。
- 高效的内存分配算法:TCMalloc使用了一些高效的内存分配算法,如Central Cache、Thread-Cache、PageHeap等,提高了内存分配和回收的效率。
TCMalloc模型相关数据结构
- Page:TCMalloc将整个虚拟内存空间划分为n个同等大小的Page,每个page默认8KB。TCMalloc是以Page为最小单位与虚拟内存进行交互。
- Span:一组连续的n个page称为一个Span。TCMalloc是以Span为单位向操作系统申请内存的。每个Span记录了第一个起始Page的编号Start,和一共有多少个连续Page的数量Length。为了方便Span和Span之间的管理,Span集合是以双向链表的形式构建。
- Size Class:TCMalloc会将对象集合划分成多个内存刻度,同属于一个刻度类别下的内存集合称之为属于一个 Size Class。
-
ThreadCache
在TCMalloc中每个线程都会有一份单独的缓存,就是ThreadCache。ThreadCache中对于每个Size Class都会有一个对应的FreeList,FreeList表示当前缓存中还有多少个空闲的内存可用。使用方对于从TCMalloc申请的小对象,会直接从TreadCache获取,实则是从FreeList中返回一个空闲的对象,如果对应的Size Class刻度下已经没有空闲的Span可以被获取了,则ThreadCache会从CentralCache中获取。当使用方使用完内存之后,归还也是直接归还给当前的ThreadCache中对应刻度下的的FreeList中。整条申请和归还的流程是不需要加锁的,因为ThreadCache为当前线程独享,但如果ThreadCache不够用,需要从CentralCache申请内存时,这个动作是需要加锁的。不同Thread之间的ThreadCache是以双向链表的结构进行关联,是为了方便TCMalloc统计和管理。
-
CentralCache
CentralCache是各个线程共用的,所以与CentralCache获取内存交互是需要加锁的。CentralCache缓存的Size Class和ThreadCache的一样,这些缓存都被放在CentralFreeList中,当ThreadCache中的某个Size Class刻度下的缓存小对象不够用,就会向CentralCache对应的Size Class刻度的CentralFreeList获取,同样的如果ThreadCache有多余的缓存对象也会退还给相应的CentralFreeList。
-
PageHeap
PageHeap是提供CentralCache的内存来源。PageHead与CentralCache不同的是CentralCache是与ThreadCache布局一模一样的缓存,主要是起到针对ThreadCache的一层二级缓存作用,且只支持小对象内存分配。而PageHeap则是针对CentralCache的三级缓存。弥补对于中对象内存和大对象内存的分配,PageHeap也是直接和操作系统虚拟内存衔接的一层缓存,当找不到ThreadCache、CentralCache、PageHeap都找不到合适的Span,PageHeap则会调用操作系统内存申请系统调用函数来从虚拟内存的堆区中取出内存填充到PageHeap当中。
TCMalloc对象分配
TCMalloc根据对象占用内存的大小分为了3中类型:小对象(0, 256KB]、中对象(256KB, 1MB]、大对象(1MB, NaN),不同的对象类型有不同的分配算法。
小对象分配
小对象为占用内存小于等于256KB的内存
- Thread 用户线程应用逻辑申请内存,当前 Thread 访问对应的 ThreadCache 获取内存,此过程不需要加锁。
- ThreadCache 得到申请内存的 SizeClass(一般向上取整,大于等于申请的内存大小),通过 SizeClass 索引去请求自身对应的 FreeList。
- 判断得到的 FreeList 是否为非空。
- 如果 FreeList 非空,则表示目前有对应内存空间供 Thread 使用,得到 FreeList 第一个空闲 Span 返回给 Thread 用户逻辑,流程结束。
- 如果 FreeList 为空,则表示目前没有对应 SizeClass 的空闲 Span 可使用,请求 CentralCache 并告知 CentralCache 具体的 SizeClass。
- CentralCache 收到请求后,加锁访问 CentralFreeList,根据 SizeClass 进行索引找到对应的 CentralFreeList。
- 判断得到的 CentralFreeList 是否为非空。
- 如果 CentralFreeList 非空,则表示目前有空闲的 Span 可使用。返回多个 Span,将这些 Span(除了第一个Span)放置 ThreadCache 的 FreeList 中,并且将第一个 Span 返回给 Thread 用户逻辑,流程结束。
- 如果 CentralFreeList 为空,则表示目前没有可用是 Span 可使用,向 PageHeap 申请对应大小的 Span。
- PageHeap 得到 CentralCache 的申请,加锁请求对应的 Page 刻度的 Span 链表。
- PageHeap 将得到的 Span 根据本次流程请求的 SizeClass 大小为刻度进行拆分,分成N份 SizeClass 大小的 Span 返回给 CentralCache,如果有多余的 Span 则放回 PageHeap 对应 Page 的 Span 链表中。
- CentralCache 得到对应的N个 Span,添加至 CentralFreeList 中,跳转至第(8)步。
中对象分配
中对象为大于256KB且小于等于1MB的内存。对于中对象申请分配的流程TCMalloc与处理小对象分配有一定的区别。对于中对象分配,Thread不再按照小对象的流程路径向 ThreadCache 获取,而是直接从 PageHeap 获取。 PageHeap 将128个Page以内大小的 Span 定义为小 Span,将128个 Page 以上大小的 Span 定义为大 Span。由于一个 Page 为8KB,那么128个 Page 即为1MB,所以对于中对象的申请,PageHeap 均是按照小 Span 的申请流程。
- Thread用户逻辑层提交内存申请处理,如果本次申请内存超过256KB但不超过1MB则属于中对象申请。TCMalloc将直接向 PageHeap 发起申请Span请求。
- PageHeap 接收到申请后需要判断本次申请是否属于小 Span(128个Page以内)。如果是,则走小Span,即中对象申请流程,如果不是,则进入大对象申请流程。
- PageHeap 根据申请的Span在小 Span 的链表中向上取整,得到最适应的第K个 Page 刻度的 Span 链表。
- 得到第K个 Page 链表刻度后,将K作为起始点,向下遍历找到第一个非空链表,直至128个 Page 刻度位置,找到则停止,将停止处的非空 Span 链表作为提供此次返回的内存 Span,将链表中的第一个 Span 取出。如果找不到非空链表,则当错本次申请为大 Span 申请,则进入大对象申请流程。
- 假设本次获取到的 Span 由N个 Page 组成。PageHeap 将N个 Page 的 Span 拆分成两个 Span,其中一个为K个 Page 组成的 Span,作为本次内存申请的返回,给到Thread,另一个为N-K个 Page 组成的 Span,重新插入到N-K个 Page 对应的 Span 链表中。
大对象分配
对于超过128个Page(即1MB)的内存分配则为大对象分配流程。大对象分配与中对象分配情况类似,Thread绕过 ThreadCache 和 CentralCache,直接向 PageHeap 获取。进入大对象分配流程除了申请的 Span 大于128个 Page 之外,对于中对象分配如果找不到非空链表也会进入大对象分配流程。
- Thread 用户逻辑层提交内存申请处理,如果本次申请内存超过1MB则属于大对象申请。TCMalloc 将直接向 PageHeap 发起申请 Span。
- PageHeap 接收到申请后需要判断本次申请是否属于小 Span(128个Page以内)。如果是,则走小Span中对象申请流程,如果不是,则进入大对象申请流程。
- PageHeap 根据 Span 的大小按照 Page 单元进行除法运算,向上取整,得到最接近 Span 的且大于 Span 的 Page 倍数K,此时的K应该是大于128。如果是从中对象流程分过来的(中对象申请流程可能没有非空链表提供Span),则K值应该小于128。
- 搜索 Large Span Set 集合,找到不小于K个Page的最小 Span(N个Page)。如果没有找到合适的 Span,则说明 PageHeap 已经无法满足需求,则向操作系统虚拟内存的堆空间申请一堆内存,将申请到的内存安置在 PageHeap 的内存结构中,重新执行(3)步骤。
- 将从 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 链表中。
Go内存分配
应用程序的内存会分成堆区(Heap)和栈区(Stack)两个部分,程序在运行期间可以主动从堆区申请内存空间,这些内存由内存分配器分配并由垃圾收集器负责回收。
- 栈内存
栈区的内存由编译器自动进行分配和释放,栈区中存储着函数的参数以及局部变量,它们会随着函数的创建而创建,函数的返回而销毁。 - 堆内存
Go语言采用现代内存分配TCMalloc算法的思想来进行堆内存的分配,将对象分为微小对象、小对象、大对象,使用三级管理结构mcache、mcentral、mheap用于管理、缓存加速span对象的访问和分配,使用精准的位图管理已分配的和未分配的对象及对象的大小。通过三级管理结构,每个线程都会自行维护一个独立的内存池(mcache),