Golang 系列 - 内存管理

90 阅读3分钟

引言

这篇文章带大家从设计思想到核心源码了解一下 Golang 的内存管理机制,学习优秀的设计思想。主要涉及的内容有:mheap, mCentral, mCache, mSpan, pageAlloc, heapArena 等核心数据结构以及 Golang 对象分配流程等, let's dive in.

核心思想

Golang 内存管理的核心思想就是以空间换时间,一次缓存,多次复用。

具体来说,Go Runtime 每次向操作系统申请内存时会多申请一些,以备后用,申请单位为 heapArena, 或者说 heapArena 指的是 Go Runtime 从操作系统申请的一块连续的内存区域。一个 heapArena 会被分成多份大小为 8KB 的 page,每个 page 又被划分为多块较小的 span,对象就被分配到这些 span 中。

通过管理和利用 heapArena 内的空闲页和 span,Go 可以高效地进行内存分配与回收。

mheap/mcentral/mcache

对于 Go 进程内部,堆是所有对象的内存起源,全局唯一对象,它对应的数据结构是 mheap,所以 mheap 也是 Go runtime 中最大的临界共享资源,这意味着每次存取都要加锁,在性能层面是一件很可怕的事情。

所以,为了提高内存分配效率,Go 采用多级缓存来实现无/细锁化地分配内存,Go在 mheap 之上,依次细化粒度,建立了 mcentral, mcache 的模型。

  • mheap: 全局的内存起源,访问要加全局锁
  • mcentral: 每种对象大小规格(全局共划分为68种)对应的缓存,锁的粒度也仅限于同一种规格以内。
  • mcache: 每个P(GMP)持有的一份内存缓存,访问时无锁

还有两个用于内存管理的数据结构:

  • page: Golang 借鉴操作系统分页管理的思想,每个最小的存储单元也称之为页(page),但大小为8KB
  • mspan: 最小的内存管理单元,大小为page的整数倍,从 8B 到 80KB 共67种规格,分配对象时,会根据大小映射到对应规格的 mspan, 从中获取空间

pageAlloc

pageAlloc 是在 Golang 堆内存中,用来加快寻找空闲页的一种索引结构,底层实现是基数树(Radix Tree 前缀树),每棵基数树聚合了16GB内存空间中各页使用情况的索引信息,用于帮助 mheap 快速找到指定长度的连续空闲页的所在位置, mheap 堆内存的上限是 256TB,所以其持有 2^14 棵基数树,因此索引能够覆盖到所有堆内存

Golang对象分配流程

在 Golang 中,无论是 new, make 还是 &T{} 表达式, 分配内存最终都由 runtime 包下的 mallocgc 方法负责.

因为内存分配本身也是触发 GC 的一个入口,当发现 mcache/mcentral 中的内存不够用了, 会将 shouldhelpgc 置为 true,发起 GC

分配的具体流程是:

  • 依据Object大小,划分为三种对象类型:Tiny Obj(0-16B), Small Obj(16B-32K), large Obj(>32K)
  • 如果是 Tiny Obj:
    1. 从P专属的 mcache 的 tiny 分配器取内存(无锁)
    2. 如果没有,则根据所属的 spanClass, 从 P 专属 mcache 的 mspan 中取(无锁)
    3. 如果没有,则根据所属的 spanClass, 从对应的 mcentral 中取 mspan 填充到 mcache,然后从 mspan 中取(spanClass粒度锁)
    4. 如果没有,则根据所属的 spanClass, 从 mheap 的页分配器 pageAlloc 找出空闲页组装成 mspan 填充到 mcache ,然后从 mspan 中取(全局锁)
    5. 如果没有,mheap 向操作系统申请内存,更新pageAlloc索引信息,然后重复 step 4
  • 如果是 Small Obj:
    • 跳过1,执行2-5
  • 如果是 Large Obj:
    • 跳过1-3,执行4-5

总结一下整个流程类似读多级缓存的过程,由上而下,每一步成功则返回,不成功继续下层处理