本文基于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的分配将舍入到页面大小,然后将页面直接分配给堆。
直接从堆进行大分配
现在,我们可以很好地了解内存分配过程中正在发生的事情。让我们将所有组件放在一起以获得完整视图:
内存分配的组成部分