28

[译] Go语言内存管理与分配

 4 years ago
source link: http://www.pengrl.com/p/38720/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

2Ib6fez.png!web

本文基于Go 1.13

Go程序的内存从申请阶段到不再使用后的释放阶段都由Go标准库自动管理。尽管管理工作不需要开发者参与,但是Go对内存管理的底层实现做了非常好的优化,里面充满了有意思的知识点,还是值得我们学习的。

从堆上申请内存

Go内存管理的设计目标是在并发环境下保持高性能,并且集成垃圾回收器。让我们从一个简单的例子开始:

package main

type smallStruct struct {
   a, b int64
   c, d float64
}

func main() {
   smallAllocation()
}

//go:noinline
func smallAllocation() *smallStruct {
   return &smallStruct{}
}

//go:noinline 这行注释可以禁止编译时的内联优化,从而避免编译时把 smallAllocation 这个函数调用直接优化没了。

运行逃逸分析命令 go tool compile "-m" main.go ,得到内存申请情况:

main.go:14:9: &smallStruct literal escapes to heap

运行 go tool compile -S main.go 命令,获取程序的汇编代码,可以更清晰的查看内存申请情况:

0x001d 00029 (main.go:14)   LEAQ   type."".smallStruct(SB), AX
0x0024 00036 (main.go:14)  PCDATA $0, $0
0x0024 00036 (main.go:14)  MOVQ   AX, (SP)
0x0028 00040 (main.go:14)  CALL   runtime.newobject(SB)

newobject 是用于申请内存的内建函数, newobjectmallocgc 的代理, mallocgc 是管理堆内存的函数。Go分配内存有两种策略:小块内存申请和大块内存申请。

小块内存申请

对于32KB以下的小块内存申请,Go会尝试从本地缓存 mcache 中获取内存。 mcache 包含了一系列被称为 mspanspan 列表, mspan 包含了可供分配使用的内存:

emYn2iE.png!web

Go的线程调度模型中,每个系统线程 M 和一个上下文 P 挂钩,在一个指定时间点最多只能处理一个协程 G 。申请内存时,当前协程会首先在所属 M 的本地缓存中的 span 列表中查找可用的内存块。使用本地缓存的好处是不用加锁,更高效。

span 列表按大小被划分为大约70个等级,大小从8字节到32K字节不等,不同等级存储不同大小的内存块:

Qfq2yeM.png!web

在我们前面的例子,结构体的大小为32字节,所以使用32字节的 span

fYZjIbv.png!web

每个等级的 span 链表会存在两份:一个链表用于存储内部不包含指针的对象,另一个链表用于存储内部包含指针的对象。这么的好处是垃圾回收时更高效,因为不需要扫描不包含指针的那个 span 链表。

译者 yoko 注:

对英文原文做个补充。

每个 mcache 包含了 2 * 67 个链表(一个元素个数为 2 * 67 的数组,数组中的一个元素即为一个 mspan 链表)。

这里的67怎么来的呢,为什么不是1呢?

实际上每个 mspan 都各自管理了一大块内存块,而每个 mspan 又被切割成n个小内存块( object ), object 才是真正分配给用户使用的内存块。

那么问题来了, mspan 按多大切割成 object 合适呢,太小可能不满足用户申请的大小,太大又造成浪费。

Go采取的策略是将32K大小以内的大小预定义了67个大小等级,每一个链表中的所有 mspan 都按该链表所设定的大小等级切割 object

这样,用户申请内存时,向上取最接近的大小等级,然后去对应的链表中的 mspan 获取可用的 object

英文原文关于这部分说的不太清楚,并且上面的两张图画得都不是太准确。实际上应该是一行可能有多个 mspan ,然后每个 mspan 内又可能包含多个 object

现在,你可能会奇怪如果 mcache 上没有空闲的内存块可供分配该怎么办。Go另外还维护了全局的 span 列表,同样也按大小分成多个级别,叫做 mcentralmcentral 包含两种链表,一张包含空闲内存块,一张包含已使用内存块:

ABJVv2m.png!web

mcentral 维护了两张 span 链表。一张链表为 non-empty 类型,包含了可供分配的 span (由于一个 span 可能包含多个 object ,只要有一个或一个以上的object可供分配即表示该 span 可供分配),一张为 empty 类型,包含已分配完毕的 span 。当Go执行垃圾回收时,如果 span 中的内存块被标记为可供分配, span 会重新加入到 non-empty 链表中。

mcentral 获取 span 的流程图如下:

N7raAnJ.png!web

mcentral 中也没有可供分配的 span 时,Go会从堆上申请新的 span 并将其放入 mcentral 中:

N3UVbau.png!web

堆在必要时向操作系统申请内存。它会申请一块大内存,被称为 arena ,在64位系统下为64MB,其它大部分系统为4MB,申请的内存同样用 span 管理:

Znia2em.png!web

大块内存申请

Go申请大于32KB的大块内存不使用本地缓存策略,而是将大小取整到页大小整数倍后直接从堆上申请。

JryqInV.png!web

全局图

现在我们在一个较高层次上,对Go的内存分配有了一个大致了解。让我们将所有的组件集合到一起来绘制一张全局图:

Mvmquem.png!web

设计灵感

Go内存分配器的设计基于TCMalloc,TCMalloc是由Google专门为并行环境优化的内存分配器。TCMalloc的 文档 很值得一读,在文档里你也能找到本文中讲解到的一些概念。

原文链接: https://pengrl.com/p/38720/

原文出处: yoko blog ( https://pengrl.com )

原文作者:yoko

版权声明:本文欢迎任何形式转载,转载时完整保留本声明信息(包含原文链接、原文出处、原文作者、版权声明)即可。本文后续所有修改都会第一时间在原始地址更新。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK