Cwww3's Blog

Record what you think

0%

go-memery

内存管理

TCMalloc

TCMalloc 为每个 Thread 预分配一块缓存,每个 Thread 在申请内存时首先会先从这个缓存区 ThreadCache 申请,且所有 ThreadCache 缓存区还共享一个叫 CentralCache 的中心缓存。这里假设目前 Golang 的内存管理用的是原生 TCMalloc 模式,那么线程与内存的关系将如图所示。

img

ThreadCache 作为线程独立的第一交互内存,访问无需加锁,CentralCache 则作为 ThreadCache 临时补充缓存,访问需要加锁。

TCMalloc 讲对象分为三类:小对象[0,256KB] 中对象(256KB,1MB] 大对象(1MB,+∞)

ThreadCache 和 CentralCache 可以解决小对象内存块的申请,对于中/大对象的申请,TCMalloc 有一个全局共享内存堆 PageHeap。

image.png

PageHeap 也是一次系统调用从虚拟内存中申请的,PageHeap 很明显是全局的,所以访问一定是要加锁。其作用是当 CentralCache 没有足够内存时会从 PageHeap 取,当 CentralCache 内存过多或者充足,则将低命中内存块退还 PageHeap。如果 Thread 需要大对象申请超过的 Cache 容纳的内存块单元大小,也会直接从 PageHeap 获取。

TCMalloc 模型相关基础结构

Page

TCMalloc 将虚拟内存空间划分为多份同等大小的 Page,每个 Page 默认是 8KB。

image.png

Span

多个连续的 Page 称之为是一个 Span。TCMalloc 是以 Span 为单位向操作系统申请内存的。Span 集合是以双向链表的形式构建。

image.png

Size Class

在 256KB 以内的小对象,TCMalloc 会将这些小对象集合划分成多个内存刻度,同属于一个刻度类别下的内存集合称之为属于一个 Size Class

每个 Size Class 都对应一个大小比如 8KB、16KB、32KB 等。在申请小对象内存的时候,TCMalloc 会根据使用方申请的空间大小就近向上取最接近的一个 Size Class 的 Span 内存块返回给使用方。**(图中的 Size Class 单位有误)**

image.png

ThreadCache

在 TCMalloc 中每个线程都会有一份单独的缓存,就是 ThreadCache。ThreadCache 中对于每个 Size Class 都会有一个对应的 FreeList,FreeList 表示当前缓存中还有多少个空闲的内存可用。

image.png

使用方对于从 TCMalloc 申请的小对象,会直接从 TreadCache 获取,实则是从 FreeList 中返回一个空闲的对象,如果对应的 Size Class 刻度下已经没有空闲的 Span 可以被获取了,则 ThreadCache 会从 CentralCache 中获取。当使用方使用完内存之后,归还也是直接归还给当前的 ThreadCache 中对应刻度下的的 FreeList 中。

不同 Thread 之间的 ThreadCache 是以双向链表的结构进行关联,是为了方便 TCMalloc 统计和管理。

CentralCache

CentralCache 是各个线程共用的,所以与 CentralCache 获取内存交互是需要加锁的。CentralCache 缓存的 Size Class 和 ThreadCache 的一样,这些缓存都被放在 CentralFreeList 中,当 ThreadCache 中的某个 Size Class 刻度下的缓存小对象不够用,就会向 CentralCache 对应的 Size Class 刻度的 CentralFreeList 获取,同样的如果 ThreadCache 有多余的缓存对象也会退还给响应的 CentralFreeList。

image.png

PageHeap

PageHead 与 CentralCache 不同的是 CentralCache 是与 ThreadCache 布局一模一样的缓存,主要是起到针对 ThreadCache 的一层二级缓存作用,且只支持小对象内存分配。而 PageHeap 则是针对 CentralCache 的三级缓存。弥补对于中对象内存和大对象内存的分配,PageHeap 也是直接和操作系统虚拟内存衔接的一层缓存,当 ThreadCache、CentralCache、PageHeap 都找不到合适的 Span,PageHeap 则会调用操作系统内存申请系统调用函数来从虚拟内存的堆区中取出内存填充到 PageHeap 当中。

image.png

PageHeap 内部的 Span 管理,采用两种不同的方式,对于 128 个 Page 以内的 Span 申请,每个 Page 刻度都会用一个链表形式的缓存来存储。对于 128 个 Page 以上内存申请,PageHeap 是以有序集合来存放。

TCMalloc 的小对象分配

image.png

TCMalloc 的中对象分配

中对象为大于 256KB 且小于等于 1MB 的内存。对于中对象申请分配的流程 TCMalloc 与处理小对象分配有一定的区别。对于中对象分配,Thread 不再按照小对象的流程路径向 ThreadCache 获取,而是直接从 PageHeap 获取,具体的流程如图所示。

image.png

PageHeap 将 128 个 Page 以内大小的 Span 定义为小 Span,将 128 个 Page 以上大小的 Span 定义为大 Span。由于一个 Page 为 8KB,那么 128 个 Page 即为 1MB,所以对于中对象的申请,PageHeap 均是按照小 Span 的申请流程。

TCMalloc 的大对象分配

对于超过 128 个 Page(即 1MB)的内存分配则为大对象分配流程。大对象分配与中对象分配情况类似,Thread 绕过 ThreadCache 和 CentralCache,直接向 PageHeap 获取。

image.png

Golang 堆内存管理

Golang 内存管理模型的逻辑层次全景图,如图所示。

image.png

Golang 内存管理单元相关概念

Page

与 TCMalloc 的 Page 一致。

mSpan

与 TCMalloc 中的 Span 一致。

Size Class

Golang 内存管理针对 Size Class 对衡量内存的概念更加详细,这里面介绍一些基础的有关内存大小的名词及算法。

  • Object Size: 应用一次向 Golang 内存申请的对象 Object 大小。 Page 是 Golang 内存管理与操作系统交互衡量内存容量的基本单元,Golang 内存管理内部本身用来给对象存储内存的基本单元是 Object。

image.png

  • Size Class

Golang 内存管理中的 Size Class 与 TCMalloc 所表示的设计含义是一致的,都表示一块内存的所属规格或者刻度。Golang 内存管理中的 Size Class 是针对 Object Size 来划分内存的。

  • Span Class

这个是 Golang 内存管理额外定义的规格属性,是针对 Span 来进行划分的,是 Span 大小的级别。一个 Size Class 会对应两个 Span Class,其中一个 Span 为存放需要 GC 扫描的对象(包含指针的对象),另一个 Span 为存放不需要 GC 扫描的对象(不包含指针的对象)

image.png

MCache

从概念来讲 MCache 与 TCMalloc 的 ThreadCache 十分相似,访问 mcache 依然不需要加锁而是直接访问,且 MCache 中依然保存各种大小的 Span。虽然 MCache 与 ThreadCache 概念相似,二者还是存在一定的区别的,MCache 是与 Golang 协程调度模型 GPM 中的 P 所绑定,而不是和线程绑定。MCache 与 P 进行绑定更能节省内存空间使用,可以保证每个 G 使用 MCache 时不需要加锁就可以获取到内存。而 TCMalloc 中的 ThreadCache 随着 Thread 的增多。

image.png

对于 Span Class 为 0 和 1 的,也就是对应 Size Class 为 0 的规格刻度内存,mcache 实际上是没有分配任何内存的。因为 Golang 内存管理对内存为 0 的数据申请做了特殊处理,如果申请的数据大小为 0 将直接返回一个固定内存地址,不会走 Golang 内存管理的正常逻辑

MCentral

当 MCache 中出现 Size Class 的 Span 空缺情况,MCache 则会向 MCentral 申请对应的 Span。Goroutine、MCache、MCentral、MHeap 互相交换的内存单位是不同,具体如图所示。

image.png

MCentral 与 TCMalloc 中的 Central 不同的是 MCentral 针对每个 Span Class 级别有两个 Span 链表,而 TCMalloc 中的 Central 只有一个。

image.png

MCentral 与 MCache 不同的是,每个级别保存的不是一个 Span,而是一个 Span List 链表。与 TCMalloc 中的 Central 不同的是,MCentral 每个级别都保存了两个 Span List

MHeap

MHeap 是对内存块的管理对象,是通过 Page 为内存单元进行管理。那么用来详细管理每一系列 Page 的结构称之为一个 HeapArena,它们的逻辑层级关系如图所示。

image.png

一个 HeapArena 占用内存 64MB,其中里面的内存的是一个一个的 mspan,当然最小单元依然是 Page,图中没有表示出 mspan,因为多个连续的 page 就是一个 mspan。所有的 HeapArena 组成的集合是一个 Arenas,也就是 MHeap 针对堆内存的管理。

image.png

Tiny 对象分配流程

Golang 内存管理将对象大小进行了分类:Tiny 对象/小对象(16B 至 32KB)/大对象

针对 Tiny 微小对象的分配,实际上 Golang 做了比较特殊的处理,之前在介绍 MCache 的时候并没有提及有关 Tiny 的存储和分配问题,MCache 中不仅保存着各个 Span Class 级别的内存块空间,还有一个比较特殊的 Tiny 存储空间。

image.png

Tiny 空间是从 Size Class = 2(对应 Span Class = 4 或 5)中获取一个 16B 的 Object,作为 Tiny 对象的分配空间。对于 Golang 内存管理为什么需要一个 Tiny 这样的 16B 空间,原因是因为如果协程逻辑层申请的内存空间小于等于 8B,那么根据正常的 Size Class 匹配会匹配到 Size Class = 1(对应 Span Class = 2 或 3),所以像 int32、 byte、 bool 以及小字符串等经常使用的 Tiny 微小对象,也都会使用从 Size Class = 1 申请的这 8B 的空间。但是类似 bool 或者 1 个字节的 byte,也都会各自独享这 8B 的空间,进而导致有一定的内存空间浪费。

image.png

小对象分配流程

image.png

大对象分配流程

小对象是在 MCache 中分配的,而大对象是直接从 MHeap 中分配。对于不满足 MCache 分配范围的对象,均是按照大对象分配流程处理。

大对象分配流程是协程逻辑层直接向 MHeap 申请对象所需要的适当 Pages,从而绕过从 MCaceh 到 MCentral 的繁琐申请内存流程,大对象的内存分配流程相对比较简单,具体的流程如图所示。

image.png

Donate comment here.