为什么要学习内存管理?
内存管理是Golang这门语言的基础,甚至于只要是计算机,都绕不开内存这个词.
其实内存管理这个概念非常简单,但它又是所有自动内存管理型语言的基本,内存管理大体上可以分为两个部分,第一个部分是内存分配,第二部分是内存回收.
TCMalloc 是google开发的内存分配算法库,最开始它是作为google的一个性能工具库 perftools(一款非常优秀的性能分析工具)的一部分。
TCMalloc是用来替代传统的malloc内存分配函数。它有减少内存碎片,适用于多核,更好的并行性支持等特性。TCMalloc中的T就是Thread线程,而C指的就是Cache缓存,Malloc顾名思义就是分配。
Golang的内存分配算法绝大部分都是来自 TCMalloc,Golang只针对其中改动了一小部分。可以说TCMalloc就是Golang内存分配的框架,所以要理解Golang内存分配算法,就要先了解下TCMalloc,为之后学习Golang的内存分配打下基础
首先让我们看看TCMalloc的整体框架
初看这张图可能会有些疑惑,不要慌,我们从左向右看,就可以把问题分解下来.
在最右的User Code其实就是我们所写的代码,TCMalloc分为三层,分别是Front-End(前端),Middle-End(中端),Back-End(后端),没错,听名字就知道,这三层和Web开发的三层架构非常相似,也是层层递进的关系.最后的OS也就是操作系统.
首先,在我们运行的代码需要内存时,会先向前端申请内存,当前端的内存缓存不足时,会像中端请求内存,中端内存不足时会向后端请求内存,后端发现也没有内存的时候,会向OS申请内存. 当然,这只是简单的一个流程概述.想要更加深入的学习TCMalloc的内存分配还需要了解几个前置概念.
Page
操作系统对内存管理的单位,TCMalloc也是以页为单位管理内存,但是TCMalloc中Page大小是操作系统中页的倍数关系. TCMalloc的默认Page大小是8KB,Linux是4KB.
Span
Span是PageHeap中管理内存页的单位,它是由一组连续的Page组成,比如2个Page组成的span,多个这样的span就用链表来管理。当然,还可以有4个Page组成的span等等。
Span可用于管理已移交给应用程序的大对象(多个Page组成的大对象),或已拆分为一系列小对象的一组页面(一个或多个Page被Size-Class拆分固定大小的Object链表)。如果Span管理的是小对象,则会在Span中记录对象的Size-Class信息。
Size-Class
由Span分裂出的对象,由同一个Span分裂出的SizeClass大小相同,SizeClass是对象内存实际的载体.
“小”对象(由参数确定)的分配被映射到60-80个不同大小的Size-class类型上。例如,一个12字节的分配将被四舍五入到16字节Size-class。Size-class的设计是为了在舍入到下一个最大的size类时尽量减少浪费的内存量。
Size-Class x只是个入口,对应的每一条链表中的Object大小相同,即由Span分割出来的对象.
Central Free List
Central Free List是当Front-End内存不足时,提供内存供Front-End使用.
Central Free List 以Span为基础管理内存,当需要内存时,会从不同的Span中提取内存.(即将Span按照Size-class拆分为Object提供给前端)
当对象返回到Central Free List时,每个对象都将映射到它所属的Span(使用pagemap,然后释放到该Span中。
每种规格的Size-Class,都从不同的 Span 进行分配;每种规则的Size-class都有一个独立的内存分配单元:CentralCache,如上图,每一个size-class都会关联一个span List,这个list中所有span的大小都是相同的,每个span都已经被拆分为对应的size-class,其中用链表维护着空闲的object,当前端有需要时会被取出.
Transfer Cache
当前端请求内存或返回内存时,都会首先到达Transfer Cache.
Transfer Cache包含一个指向可用内存的指针数组,可以快速将对象移动到该数组中,或者代表前端从该数组中获取对象。
Transfer Cache的名称来自一个线程正在分配由另一个线程释放的内存的情况。 传输缓存允许内存在两个不同线程之间快速流动。
如果传输缓存无法满足内存请求,或者没有足够的空间容纳返回的对象,它将访问Central free List。
PageHeap
PageHeap保存的也是若干链表,不过链表保存的是Span。
Central Free List内存不足时,可以从PageHeap获取Span,然后把Span切割成对应Size-class大小的Object 返回前端.
Pagemap and Spans
所有被TCMalloc从操作系统被申请到的内存会被划分成编译时就被确认好的Page大小, pagemap用于查找对象所属的Span,或标识给定对象的Size-class(例如一个大对象被分配了多个Page的Span,或者一个Span被拆分成Size-class大小后分配给小对象)。
TCMalloc使用2级或3级基数树来映射所有申请到的内存.
在介绍完前置概念以后,就可以正式深入TCMalloc的分配流程了.
回到起点,当我们的代码需要内存时,就会先按照策略向Front-End申请内存, 它是一个内存缓存,提供了快速分配和重分配内存给应用的功能。有两种实现: Per-thread cache 和 Per-CPU cache.
前者基于线程分配内存,每个线程都有一定的内存缓存.
缺点:现代的程序大都有比较多的线程数量,会造成大量线程聚合即缓存内存过大,或者每个线程的内存缓存较少.
后者基于CPU分配内存,每个CPU都有一定的内存缓存.
注:在x86的架构里,一个逻辑CPU相当于一个超线程.(例如6核12线程里的12线程就是逻辑CPU)
来到前端的请求分为两种,由一个kMaxSize的参数值来界定
前端用于处理大部分的内存的请求(Size-class类型包含了大部分请求的内存请求)。前端具有内存缓存,可用于分配或保存可用内存。该高速缓存一次只能由单个线程(CPU)访问,因此它不需要任何锁,因此大多数分配和释放都是快速的。
如果前端已缓存适当大小的内存,它可以满足大部分的请求。但是如果某种特定大小的缓存为空,则前端将向中端(Middle-End)请求一批内存以重新填充缓存。 中间端包括Central Free List和Transfer Cache。
当前端请求内存或返回内存时,它将到达传输缓存。
传输缓存包含一个指向可用内存的指针数组,可以快速将对象移动到该数组中,或者代表前端从该数组中获取对象。
传输缓存的名称来自一个线程正在分配由另一个线程释放的内存的情况。 传输缓存允许内存在两个不同线程之间快速流动。
如果传输缓存无法满足内存请求,或者没有足够的空间容纳返回的对象,它将访问中央空闲列表。
Central Free List是当Front-End内存不足时,提供内存供其使用.
Central Free List 以Span为基础管理内存,当需要内存时,会从不同的Span中提取内存.
当对象返回到Central Free List时,每个对象都将映射到它所属的Span(使用pagemap,然后释放到该跨度中。如果驻留在特定跨度中的所有对象都返回给它,则整个跨度 返回到后端。
如果中端已用尽,将请求后端分配Page填充到中端. 后端也称为PageHeap.
TCMalloc的后端有三个任务:
TCMalloc有两个类型的后端:
当中端内存不够的请求来到后端时,后端会返回对应Span的内存给中端.
至此TCMalloc的内存分配方式就基本介绍完了,如果TCMalloc的分配方式你已经学会了,那相信下一篇的Golang内存分配也已经轻而易举了.
参考: