ℹ️本文基于Go1.13
当不再使用内存时,标准库会自动执行Go的内存管理即从分配到回收。尽管开发者不需要处理它,但是Go的底层管理进行了很好的优化并且充满了有趣的概念。
堆上的分配
内存管理被设计可以在并发环境快速执行并且集成了gc。让我们从一个例子开始:
package main type smallStruct struct { a, b int64 c, d float64 } func main() { smallAllocation() } //go:noinline func smallAllocation() *smallStruct { return &smallStruct{} }
注释//go:noinline
将会阻止内联优化,以避免内联通过移除函数的方式优化这段代码,从而造成最终没有分配内存的情况。
运行逃逸分析命令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
缓存中获取内存。此缓存包含一组mspan
每个M
被分配给一个处理器P
并且一次只能处理一个goroutine。当需要分配内存时,当前goroutine会使用它当前P
的本地缓存来从中寻找第一个可用空闲对象。使用本地缓存不需要加锁会使得分配更加高效。
mspan被分为约70个尺寸类型,从8字节到32k字节。
每个mspan会存在2次:一个不包含指针,一个包含指针。这种区别会使得gc更加容易因为它不需要扫描那些不包含指针的mspan。
在我们之前的例子里,结构体是32字节所以它适合于32 字节的mspan。
现在会疑惑如果mspan在内存分配时候没有空闲插槽会发生什么。Go维护了包含全尺寸类型的中央链表mcentral
,其中包含空闲和非空闲对象的mspan:
mcentral
维护着mspan的双向链表; 在非空链表(non-empty list:尚有空闲object的mspan链表) — 非空(“non-empty” )代表链表中至少有一个插槽是空闲可供分配 — 可能包含一些正在使用的内存。当gc 清理内存时,他会清理一部分mspan标记不再使用,并放回非空链表(non-empty list)
我们程序可以在插槽耗尽后向中央链表申请mspan:
如果空链表中没有可用的mspan,Go需要为中央链表获取新的mspan。新的mspan会从堆上分配并链接到中央链表上:
堆在需要时从OS中提取内存。 如果需要更多内存,堆会分配一个叫做 arena
的大块内存, 在 64 位架构下为 64Mb,在其他架构下大多为 4Mb。arena
同样使用mspan来映射内存:
大分配
Go并不适用本地缓存来管理较大的内存空间分配。对于超过 32kb 的分配,会向上取整到页的大小,并直接从堆上分配。
全景图
现在我们对内存分配的时候发生了什么有了更好的认识。现在将所有的组成部分放在一起来得到全景图: