基本概念
Golang 的 runtime 将堆地址空间划分为 arena。在 Linux 环境下每个 arena 是 64 MB,每个 arena 中又有 8192 个 page,每个 page 为 8KB。
为了防止出现出现大量内存碎片,Golang 采用了类似于 Google 的 tcmalloc 的内存分配算法:将堆内存按一组预设大小规格划分成内存块,再把这些不同规格的内存块以链表的形式组织。
在申请内存时,会先找到最匹配的规格,去对应的空闲链表中获取一个内存块。这样可以降低外部内存碎片的概率。
Golang 中有 67 种预设规格,从 8B 到 32KB。所以 arena 中会按需划分出不同的 span,每个 span 包含一组连续的 page,并且按照特定规格划分成了等大的内存块。
涉及堆内存管理的结构体
arena、span、page 和内存块组成了 Golang 堆内存的堆内存 heap,它们自然也有各自对应的结构体来管理堆内存。
heapArena
heapArena 结构体对应 arena:
1 |
|
其中存储着 arena 的元数据,有很多位图标记:
- bitmap:用一位标记这个 arena 中,一个指针大小的内存单元是指针还是标量,再用一位标记这块内存空间的后续单元是否包含指针。用于 GC 扫描阶段。
- pageInUse:用于标记处于使用状态的 span 的第一个 page。
- pageMarks:用于标记每个 span 的第一个 page,在 GC 标记阶段会修改这个位图,标记哪些 span 中存在被标记的对象,在 GC 清扫阶段根据这个位图来释放不含标记对象的 span。
- spans:用来标记某个 page 对应哪一个 span。
mspan
mspan 中管理 span 中一组连续的 page:
1 |
|
该 span 具体被划分成了什么规格大小跟 spanclass 有关,spanclass 的高 7 位用于标记内存规格的编号,对应 1-67,编号 0 被留出来表示大于 32KB 的内存,一共 68 中。
而 spanclass 的最低位用于标记这个 span 是否需要进行 GC 扫描,所以 span 又可以分为 scannable 和 non-scannable 两种,总共就是 136 种。
span 中的 nelems 记录该 span 被划分成多少内存块,而 freeIndex 记录这下一个空闲内存块的索引。
span 中也有一些位图:
- allocBits:标记哪些内存块被分配了;
- gcmarkBits:用于在 GC 扫描阶段标记内存块。
在 GC 清扫阶段会释放掉旧的 allocBits,把标记好的 gcmarkBits 用作 allocBits,因为没有在 gcmarkBits 标记下的内存块都被回收了,接着再重新分配一段清零的内存给 gcmarkBits。
mcentral
mcentral 是一个全局的 mspan 管理中心。每一种 span 对应一个 mcentral,所以 mcentral 也有 136 种。mcentral 将 mspan 按照用尽与未用尽的分别管理,而用尽的或未用尽的又会分成已清扫和未清扫的进行管理。
mcache
由于使用全局的 mspan 管理中心会导致多个 P 之间对锁产生竞争,所以每个 P 也设置了自己的本地缓存,即 mcache。
当 P 想要获取特定规格的 mspan 时,先从本地的 mcache 中尝试获取,如果没有或用完了就去 mcentral 中找一个放到本地,把已经用尽的归还到 mcentral 中对应的 full set 中。
堆内存分配过程
堆内存分配的主要逻辑可以分为四个部分:
- 辅助 GC;
- 空间分配;
- 位图标记;
辅助 GC
考虑一种情况:当内存申请的速度超过了 GC 标记的速度,内存就分配不过来了。所以需要申请内存的协程参与到 GC 中来,即辅助 GC。要帮助扫描或清扫一部分内存之后,才可以得到内存。
每次参与辅助 GC 最少需要扫描 64KB,所有如果要申请的内存并没有到达 64KB,则多干的活可以留到下次再使用。
同时 mark worker 在进行清扫的时候,也会在全局 gcController 中的 bgScanCredit 中累积信用,这个信用是可以交由辅助 GC 的协程使用的。
空间分配
在辅助 GC 完成后就可以进行空间分配了。要根据需要分配的空间大小以及是否为 non-scannable 型的空间来使用不同的分配策略:
- 当需要的空间小于 16B,并且为 non-scannable 空间的申请,就是用 tiny-allocator;
- 如果大于 32KB 则会直接分配到堆上;
- 大于等于 16B 且小于等于 32KB 的 non-scannable 空间和小于等于 32KB 的 scannable 空间的申请则按照预设大小规格来进行合适的分配。
由于最小的预设规格为 8B,所以即使只需要 1B 的空间,按预设规格来分配也会分配出去 8B,所以每个 P 都有一个 16B 大小的 tiny 内存空间,可以将小的内存分配请求统一分到 tiny 内存空间来提高内存利用率。
位图标记
将 heapArena、mspan 中对应的位图进行标记。