当不再使用内存时,标准库会自动执行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,也将向我们明确显示分配:
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。此缓存处理一个跨度列表(32kb的内存块),称为mspan,其中包含可用于分配的内存:
每个线程M都分配给一个处理器P,一次最多处理一个goroutine。在分配内存时,当前的goroutine将使用其当前的本地缓存P来查找范围列表中可用的第一个空闲对象。使用此本地缓存不需要锁定,并使分配效率更高。
范围列表分为8个字节到32k字节的70个大小类别,可以存储不同的对象大小:
每个跨度存在两次:一个不包含指针的对象的列表,另一个包含指针的对象的列表。这种区别将使垃圾收集器的寿命更加轻松,因为它不必扫描不包含任何指针的范围。
在我们之前的示例中,结构的大小为32个字节,将适合32个字节的跨度:
现在,我们可能想知道如果跨度在分配期间没有空闲时隙,将会发生什么情况。Go维护每个大小类的跨度的中央列表,称为mcentral,其中跨度包含可用对象,而跨度包含自由对象:
mcentral维护跨度的双链表;它们每个都有对上一个跨度和下一个跨度的引用。非空列表中的跨度(“非空”表示列表中至少有一个空闲插槽可供分配)可能已经包含一些正在使用的内存。确实,当垃圾收集器扫描内存时,它可以清除跨度的一部分(标记为不再使用的那一部分),并将其放回非空列表中。
现在,我们的程序可以在没有插槽的情况下从中央列表中请求跨度:
如果空列表中没有新的跨度,Go需要一种方法来将新的跨度移到中心列表。现在将从堆中分配新的范围,并将其链接到中央列表:
堆在需要时从OS中提取内存。如果需要更多的内存,堆将为arena64位体系结构分配一个称为64Mb的大块内存,对于其他大多数体系结构则分配为4Mb。舞台还使用跨度映射内存页面:
Go不会使用本地缓存来管理大量分配。这些大于32kb的分配将四舍五入为页面大小,并将页面直接分配给堆。
现在,我们可以很好地了解内存分配过程中正在发生的事情。让我们将所有组件放在一起以获得全貌: