Go: 内存管理和分配

ℹ️本文基于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 的分配,会向上取整到页的大小,并直接从堆上分配。

全景图

现在我们对内存分配的时候发生了什么有了更好的认识。现在将所有的组成部分放在一起来得到全景图:

 

编译整理自  Go: Memory Management and Allocation

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据