我发现搞懂 Go 语言内存对象分配,真的没有那么简单。为什么要搞懂 Go 语言的内存分配呢,吃饱了撑的呢!我计划涉猎多些博客,能弥补这块的知识缺失。但也可能中途就放弃了…
下图是截取自 《Go语言变编程入门和实战技巧》的这本书中的示例图,Go 语言使用了类似这样的内存分配实现。TCMalloc 内存管理体系分为三个层次:ThreadCache、CentralCache、PageHeap。采用了三级的内存管理,分配内存和释放内存都从 ThreadCache 开始,向上逐级尝试。
内存管理的整体思路:首先在 ThreadCache 分配内存,如果内存分配失败,则从下一层的 CentralCache 中补一批上来,还不够的话就再从 PageHeap 申请,再不够就向操作系统申请。释放内存也类似,逐级归还。
这里重点强调一下 ThreadCache 这个对象,很容易让人想到 Go 调度中 M 对象,M 扮演这线程的角色。在 P 对象的机构体中包含一个 mcahce 对象,虽然 mcache 没有声明在 M 结构体中,但 mcache 实际就是图中的 ThreadCache 对象。
再次强调一下 mcache 对象,我们阅读源码库的对 mcache 的注释,关注到三个细节点:小对象、单线程、无锁操作。让我们思路重新回到 TCMalloc 这张图上。
// Per-thread (in Go, per-P) cache for small objects.
// This includes a small object cache and local allocation stats.
// No locking needed because it is per-thread (per-P).
//
// mcaches are allocated from non-GC'd memory, so any heap pointers
// must be specially handled.
//
如果只是看上面那张图,感觉已经把该囊括的都囊括了,但又觉得只是蜻蜓点水。我认真了读了这本书对于 TCMalloc 的解释,每次都感觉是一头雾水。我不觉得作者写的有问题,我感觉自己的理解有问题。
我有查看了 TCMalloc : Thread-Caching Malloc 中对于 TCMalloc 的介绍,和第一张图相比,这张图在介绍的时候,拆成了三个模块:front-end 负责快速分配、回收内存;middle-end 用于补充 front-end 所需内存;back-end 用来从操作系统申请内存。
但是,我们依旧能看到 Per-thread、Center free list、page heap 这几个重复的角色,两者相互佐证一下,TCMalloc 可能真的就是这样子。
关于这三层结构,我优化要说:确定模块的边界非常重要,不同的模块有明确的不同职责。这样的设计其实非常常见,我们在设计缓存架构时,常见的三层查询:本地内存 -> Redis -> DB,本质上其实差不太多。
下面计划深入一些细节,这里的概念理解也相当重要。TCMalloc 设计了两种内存管理形式,分别是 span 和 object。其中,span 是由一组连续 page 组成的内存块,它由 central 进行管理;object 是将 span 按照特定规格进行切割,固定尺寸的小块内存。
之所以在特别强调一下 object,是因为别把这个 object 理解错了,在这篇文章里,它就描述固定尺寸的小块内存。
接下里,我要开始翻另一篇 TCMalloc : Thread-Caching Malloc 文章,关于小块内存的的申请。ThreadCache 主要负责小块内存的分配,根据需要申请的内存大小计算对应的 size class,然后从对应的 object 链表中获取 object。
如下面这张图所示,每个 size class 都对应了一个 object 链表。如果查找到的 object 链表非空,我们直接移除链表的头结点并返回。如果为空,就向 central free list 申请一批该尺寸的 的 object 加入到链表中,重新执行非空链表的获取逻辑。
关于提到的 object,该如何在代码中体现出来呢?每个 size class 对应一个 object 链表,类似于一个本地对象池。在这个池子为空的时候,可以申请向池子中添加 object。但是当池子满了的话,后续释放的 object 就无法放回池子,这部分 object 就需要被回收。池子的初始尺寸多大?该如何回收哪些无法放回池子的 object?
我们继续看大对象的申请,这和小对象的申请是明显区分的。针对超出 size class 范围的内存对象,会按照 page 为单位进行申请,如下图所示,它和小对象的 class size 类似,也是从小到大的等级链表。不过,这里的申请逻辑和小对象申请有明显的差异。
比如,我需要申请 3 pages 的内存,但是 3 pages 对应的链表为空,我会继续看 4 pages 是否有可用的内存,直到最终找到一个可以可用的等级内存,假设 4 pages 链表也为空,我最终匹配到了 5 pages 的内存,但我实际值需要 3 pages 的内容,那多出来的 2 pages 会被插入到合适的链表中。
因为申请到更大的尺寸,而多出来的 pages,是如何分配插入其他合适链表的呢?这个也很重要,比如上面多出来的 2 pages,是插入到 2pages 对应的链表呢?还是拆分成 2 个独立的 page,插入到 1page 对应的链表?万一1 page 和 2 pages 对应的链表非空元素特别多怎么办?
CentralCache 属于 ThreadCache 和 PageHeap 的中间人,负责将 PageHeap 中的内存切成小块,在恰当的时机分配给 ThreadCacche。在释放内存方面,CentralFreeList 获取从 ThreadCache 中回收的内存并在恰当的时机归还给 PageHeap。
ThreadCache 属于线程管理的内存,对象所需的内存申请在线程内部完成,不涉及到加锁的开销,主要负责小对象的内存分配。PageHeap 属于中央堆分配器,被所有线程共享,分享时需要加锁,负责与操作系统直接交互,并且大尺寸的内存申请直接通过 PageHeap 进行分配。
关于 span 和 class 的概念,内存的最小单位是 page,而 span 用来对 page 做管理,不同类型的 span 由不同数量的 page 组成,下图中,span A 由 2 个page 组成,span B 由 3 个 page 组成。这是一个对象关系的层次结构,用来记录 object 最终分配的内存位置。
关于 span 还是