内存管理涉及初始化、分配以及回收三个方面。文中使用Go版本为1.17.6
概览
为减少分配时的竞争,go内存管理借鉴TCMALLOC的分级管理思想,每一个P(G-M-P,参考Go的调度管理)有自己的内存结构mcache,同一个P内的G分配内存时不需要加锁。当P内无内存可分配时,通过加锁,使用mcenter中的内存,当mcenter中也无内存时,通过mheap申请新的内存。
mheap结构中通过heapArena管理内存申请,每一个heapArena大小为64M,包括8192个pages,每个page的大小为8KB。
mcache中具体的内存管理单元是mspan,mspan分为68个级别,从8字节到32KB到大对象,如果分配一个13字节的对象,则需要使用一个大小为16字节的mspan
一个mspan可以占用一个或者多个page。总体概览图如下:
结构体的关键成员如下所示:
1 | type mspan struct { |
页分配器概览
为了加快从mheap中申请pages的速度,1.17.6中的page allocator使用了一个分为5层的radix tree,tree中的每个叶子节点为一个chunk的summary,一个chunk包括512个pages,summary其实就是一个bitmap的汇总,每一个bit代表一个page是否使用,1为使用,0为未使用。summary会汇总一个chunk开头(start)、结尾(end)以及中间最大的连续0的个数(max),即可分配的连续pages个数。每8个叶子节点会再向上汇总,再形成一个8 chunks的summary,包括的内容还是start、max、end,注意当8个chunks汇总时,两个chunk的start和end可以合并,如果其值大于max,需要更新max的值。
为什么是5层呢,因为按5层计算,所有的叶子节点可表示的内存空间为 2^48字节,正好是48位地址总线能够寻找的最大地址。
入口
分配内存的入口是src/runtime/malloc.go中的mallocgc
1 | func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { |
大对象分配:
分配后也会放到span中,调用链为mcache.allocLarge()->mheap.alloc->mheap.allocspan(),mheap.allocspan函数首先判断分配pages个数,如果较少,会首先在每个P自己的cache进行page分配,否则通过页分配器进行页面分配
小对象分配:
会从mcache自己管理的span中进行分配,首先调用nextFreeFast,如果不行则继续调用nextFree。两个函数解释如下
- nextFreeFast :通过一个64bit大小的alloCache来快速计算,看是否有空闲位置,详细代码及注释如下:
1 | func nextFreeFast(s *mspan) gclinkptr { |
- nextFree:通过freeindex查找可用元素位置,如果该span已经满,则放回到mcentral,然后从mcentral重新获取一个span,如果mcentral中没有空闲span,调用mheap获取,调用链路为mcache.refill->mcentral.cacheSpan->mheap.alloc,可以看到,如果没有从mcache和mcentral中获取到span,最终又回到了大对象分配的链路
微小对象分配:
微小对象为不含指针,并且大小小于16字节的内存空间分配
mcache中tiny,tinyoffset,tinyAllocs分别表示分配的地址,当前的偏移以及分配的元素个数。如果能分配则直接分配,否则调用nextFreeFast和nextFree从16字节大小的span中获取一个新的地址进行分配。
总结
本文从最上层的入口开始探究Go语言内存管理,最上层入口为mallocgc,涉及到的结构包括mspan、mcache、mcenter、mheap,分级管理,分类分配。未涉及到具体的页分配以及内存回收。内存回收涉及到了Go的GC流程,另文再述。