写点什么

Go: 内存管理和分配

用户头像
陈思敏捷
关注
发布于: 2020 年 06 月 14 日
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

博客地址: https://www.chenjie.info/2425



本文首发于我的公众号:



发布于: 2020 年 06 月 14 日阅读数: 92
用户头像

陈思敏捷

关注

多动脑不痴呆 2017.12.21 加入

gopher

评论

发布
暂无评论
Go: 内存管理和分配