内存管理缓存结构
Go实现的内存管理采用了tcmalloc这种架构,并配合goroutine和垃圾回收。tcmalloc的基本策略就是将内存分为多个级别。申请对象优先从最小级别的内存管理集合mcache
中获取,若mcache无法命中则需要向mcentral申请一批内存块缓存到本地mcache中,若mcentral
无空闲的内存块,则向mheap
申请来填充mcentral,最后向系统申请。
mcache + mspan
最小级别的内存块管理集合mcache
由goroutine自己维护,这样从中申请内存不用加锁。它是一个大小为67的数组,不同的index对应不同规格的mspan
。newobject
的时候通过sizetoclass
计算对应的规格,然后在mcache中获取mspan对象。
1 | type mcache struct { |
mspan
包含着一批大小相同的空闲的object
,由freelist指针查找。mspan内部的object是连续内存块,即连续的n个page(4KB)的连续内存空间。然后这块空间被平均分成了规格相同的object,这些object又连接成链表。当newobject时找到mcache中对应规格的mspan,从它的freelist取一个object即可。
1 | type mspan struct { |
mheap + mcentral
如果某个规格的span里已经没有freeObject了 需要从mcentral
当中获取这种规格的mspan。正好mcentral也是按照class规格存储在数组中,只要按规格去mheap
的mcentral数组取mspan就好。
1 | // 某种规格的mspan正好对应一个mcentral |
如果central数组中这种规格的mcentral没有freeSpan了,则需要从mheap
的free
数组获取。这里规格并不对齐,所以应该要重新切分成相应规格的mspan。
1 | type mheap struct { |
内存的初始化
很早之前看过这个图,当时对他的理解有误,因为看漏了一句话 struct Mcache alloc from 'cachealloc' by FixAlloc
。就是说用户进程newobject是从下图的arena区域分配的,而runtime层自身管理的结构 比如mcache等是专门设计了fixAlloc来分配的,原因可能是这些runtime层的管理对象类型和长度都相对固定,而且生命周期很长,不适合占用arena区域。
mallocinit
通过sysReserve
向系统申请一块连续的内存 spans+bitmap+arena。其中arena为各个级别缓存结构提供的分配的内存块,spans是个指针数组用来按照page寻址arena区域。
最终sysReserve调用的是系统调用
mmap
。申请了512GB的虚拟地址空间,真正的物理内存则是用到的时候发生缺页才真实占用的。
1 | func mallocinit() { |
mheap初始化相关指针,使之可以寻址arena这块内存。同时初始化cachealloc这个固定分配器。最后执行的 m.mcache = allocmcache()
是每个gouroutine创建时都要初始化的。直到这时才真正创建了mcache
,并且初始化mcache里整个数组对应的mspan为emptyspan。
1 | func (h *mheap) init(spansStart, spansBytes uintptr) { |
fixalloc
fixalloc分配器通过init初始化每次分配的size。chunk是每次分配的固定大小的内存块,list是内存块链表。当fixalloc初始化为cachealloc时,每次调用alloc就分配一块mcache。persistantalloc看起来是runtime有个全局存储的后备内存的地方,优先从这儿取没有再从系统mmap一块。
1 | type fixalloc struct { |
内存分配
mallocgc
以下总结了malloc的流程,基本普通的小对象都是从mcache中找到相应规格的mspan,在其中的freelist上拿到object对象内存块。nextfree
中隐藏了整个内存数据块的查找和流向。
1 | func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { |
refill + cachespan
如果nextfree
在mcache相应规格的mspan里拿不到object那么需要从mcentral中refill
内存块。
这里面有个细节要将alloc中原本已经没有可用object的这块mspan还给central,应该要放进central的empty链表中。这里只是把相应的mspan的incache设置为false,等待sweep的回收。
1 | func (c *mcache) refill(sizeclass int32) *mspan { |
sweepgen是个回收标记,当sweepgen=sg-2时表示等待回收,sweepgen-1表示正在回收,sweepgen表示已经回收。从mcentral中获取mspan时有可能当前的span正在等待或正在回收,我们把等待回收的mspan可以返回用来refill mcache,因此将它insert到empty链表中。
1 | func (c *mcentral) cacheSpan() *mspan { |
mcentral grow
如果mcentral中没有mspan可以用 那么需要grow,即从mheap中获取。要计算出当前规格对应的page数目,从mheap中直接去nPage的mspan。free区域是个指针数组,每个指针对应一个mspan的链表,数组按照npage寻址。若大于要求的npage的链表中 都没有空闲mspan,则mheap也需要扩张。
1 | func (c *mcentral) grow() *mspan { |
mheap grow
mheap的扩张h.sysAlloc
直接向arena区域申请nbytes的内存,数目按照npage大小计算。arena区域的一些指针标记开始移动,最终将mspan加入链表,等待分配。
1 | func (h *mheap) grow(npage uintptr) bool { |
内存回收与释放
简单说两句:mspan里有sweepgen回收标记,回收的内存会先全部回到mcentral。如果已经回收所有的mspan那么可以返还给mheap的freelist。回收的内存块当然是为了复用,并不直接释放。
1 | func (s *mspan) sweep(preserve bool) bool { |
监控线程sysmon又出现了,它会遍历mheap中所有的free freelarge里的mspan,发现空闲时间超过阈值就madvise
建议内核释放它相关的物理内存。