本文基于Go 1.13
Go程序的内存从申请阶段到不再使用后的释放阶段都由Go标准库自动管理。尽管管理工作不需要开发者参与,但是Go对内存管理的底层实现做了非常好的优化,里面充满了有意思的知识点,还是值得我们学习的。
从堆上申请内存
Go内存管理的设计目标是在并发环境下保持高性能,并且集成垃圾回收器。让我们从一个简单的例子开始:
1 | package main |
//go:noinline
这行注释可以禁止编译时的内联优化,从而避免编译时把smallAllocation
这个函数调用直接优化没了。
运行逃逸分析命令go tool compile "-m" main.go
,得到内存申请情况:
1 | main.go:14:9: &smallStruct literal escapes to heap |
运行go tool compile -S main.go
命令,获取程序的汇编代码,可以更清晰的查看内存申请情况:
1 | 0x001d 00029 (main.go:14) LEAQ type."".smallStruct(SB), AX |
newobject
是用于申请内存的内建函数,newobject
是mallocgc
的代理,mallocgc
是管理堆内存的函数。Go分配内存有两种策略:小块内存申请和大块内存申请。
小块内存申请
对于32KB以下的小块内存申请,Go会尝试从本地缓存mcache
中获取内存。mcache
包含了一系列被称为mspan
的span
列表,mspan
包含了可供分配使用的内存:
Go的线程调度模型中,每个系统线程M
和一个上下文P
挂钩,在一个指定时间点最多只能处理一个协程G
。申请内存时,当前协程会首先在所属M
的本地缓存中的span
列表中查找可用的内存块。使用本地缓存的好处是不用加锁,更高效。
span
列表按大小被划分为大约70个等级,大小从8字节到32K字节不等,不同等级存储不同大小的内存块:
每个等级的span
会存在两份:一个用于存储内部不包含指针的对象,另一个用于存储内部包含指针的对象。这么的好处是垃圾回收时更高效,因为不需要扫描不包含指针的那个span
。
在我们前面的例子,结构体的大小为32字节,所以使用32字节的span
:
现在,你可能会奇怪如果mcache
上没有空闲的内存块可供分配该怎么办。Go另外还维护了全局的span
列表,同样也按大小分成多个级别,叫做mcentral
。mcentral
包含两种链表,一张包含空闲内存块,一张包含已使用内存块:
mcentral
维护了两张span
链表。一张链表为non-empty
类型,包含了可供分配的span
(由于一个span
可能包含多个object
,只要有一个或一个以上的object可供分配即表示该span
可供分配),一张为empty
类型,包含已分配完毕的span
。当Go执行垃圾回收时,如果span
中的内存块被标记为可供分配,span
会重新加入到non-empty
链表中。
从mcentral
获取span
的流程图如下:
当mcentral
中也没有可供分配的span
时,Go会从堆上申请新的span
并将其放入mcentral
中:
堆在必要时向操作系统申请内存。它会申请一块大内存,被称为arena
,在64位系统下为64MB,其它大部分系统为4MB,申请的内存同样用span
管理:
大块内存申请
Go申请大于32KB的大块内存不使用本地缓存策略,而是将大小取整到页大小整数倍后直接从堆上申请。
全局图
现在我们在一个较高层次上,对Go的内存分配有了一个大致了解。让我们将所有的组件集合到一起来绘制一张全局图:
设计灵感
Go内存分配器的设计基于TCMalloc,TCMalloc是由Google专门为并行环境优化的内存分配器。TCMalloc的文档很值得一读,在文档里你也能找到本文中讲解到的一些概念。
英文原文地址:https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44
本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/38720/