「译文」Go 语言内存管理与分配

1,144 阅读4分钟

内存管理与分配

原文:Memory Management and Allocation

本文基于Go1.13

当内存不被使用时,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 将禁用通过删除函数来优化代码的内联,因此最终没有分配。

运行 Escape Analysis 命令 go tool compile "-m" main.go将确认Go所做的分配:

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

通过 go tool compile -S main.go,dump 该程序的汇编代码,很清楚地显示该程序内存如何被分配的:

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 是新分配和代理 mallocgc(用于在堆上管理分配)的内置函数。Go中有两种策略,一种用于较小的分配,一种用于较大的分配。

小分配

对于32kb以下的小分配,Go会尝试从本地缓存中获取,并称之为mcache。此缓存会维护一个span列表(32kb的内存块),称为mspan,其中包含可用于分配的内存:

每个线程M都分配给一个处理器P,一次最多处理一个goroutine。在分配内存时,当前的goroutine将使用其当前的本地缓存P来查找span列表中可用的第一个空闲对象。使用此本地缓存不需要锁定,并使分配效率更高。

span列表可以存储不同的对象大小分为8个字节到32k字节的70个大小类别。

每个span存在两次:一个不包含指针的对象列表和另一个包含指针的对象列表。这种区别将使垃圾收集的工作更容易,因为它不必扫描不包含任何指针的范围。

在我们之前的示例中,结构的大小为32个字节,并会被32个字节的填充的span:

现在,我们可能想知道如果span在分配期间没有空闲插槽,将会发生什么?Go维护每个大小类别的span的中心列表,称为mcentral,其中span包含自由对象和非自有对象:

mcentral 是一个维护着span的双链表;他们每个节点都有上一个span和下一个span的引用。非空列表中的span(“非空”表示列表中至少有一个空闲插槽可供分配)可能已经包含一些正在使用的内存。确实,当垃圾收集器清除内存时,它可以清除span的一部分(标记为不再使用的那一部分),并将其放回非空列表中。

现在,我们的程序可以在没有插槽的情况下从中央列表请求span:

如果空列表中没有新的span,Go需要一种方法来将新的span移到中心列表。现在将从堆中分配新的范围,并将其链接到中央列表:

堆在需要时从OS中请求内存。如果需要更多的内存,堆将为称为arena的64位体系结构分配称为64Mb 的大量内存,对于其他大多数体系结构则分配4Mb。arena还使用内存映射来为span映射内存页面:

大量分配

Go不会使用本地缓存来管理大量分配。这些大于32kb的分配将舍入到页面大小,然后将页面直接分配给堆。

直接从堆进行大分配

现在,我们可以很好地了解内存分配过程中正在发生的事情。让我们将所有组件放在一起以获得完整视图:

内存分配的组成部分

灵感

内存分配器最初基于TCMalloc,TCMalloc是Google创建的并发环境下优化的内存分配器。感兴趣可以阅读:TCMalloc