0. 简介

程序中的数据都会被分配到程序所在的虚拟内存中,内存空间包含两个重要区域:栈(Stack)堆(Heap)。函数调用的参数、返回值和局部变量大部分会分配在栈上,这部分由编译器管理。堆内存的管理方式视语言而定:

  • C/C++等编程语言的堆内存由工程师主动申请和释放;
  • Go、Java等编程语言由工程师和编译器/运行时共同管理,其内存由内存分配器分配,由垃圾回收器回收。

本文就介绍一下Go语言的内存分配器。

1. Go内存分配设计原理

TCMallocThread-Caching Malloc

图片来自链接,侵删!

如上图所示,是Go的内存管理模型示意图,在堆内存管理上分为三个内存级别:

  • 线程缓存(MCache):作为线程独立的内存池,与线程的第一交互内存,访问无需加锁;
  • 中心缓存(MCentral):作为线程缓存的下一级,是多个线程共享的,所以访问时需要加锁;
  • 页堆(MHeap):中心缓存的下一级,在遇到32KB以上的对象时,会直接选择页堆分配大内存,而当页堆内存不够时,则会通过系统调用向系统申请内存。
mspan
//go:notinheap
type mspan struct {
   next *mspan     // next span in list, or nil if none
   prev *mspan     // previous span in list, or nil if none
   list *mSpanList // For debugging. TODO: Remove.

   startAddr uintptr // address of first byte of span aka s.base()
   npages    uintptr // number of pages in span
   
   
   freeindex uintptr

   allocBits  *gcBits
   gcmarkBits *gcBits
   allocCache uint64
   ...
}
runtime.mspannextprevruntime.mspan
startAddrmspannpages

其它字段:

freeindexallocBitsgcmarkBitsallocCacheallocBits
//go:notinheapmspan

图示:

跨度类

mspanspanclass
//go:notinheap
type mspan struct {
   ...
   spanclass   spanClass     // size class and noscan (uint8)
   ...
}
runtime.class_to_sizeruntime.class_to_allocnpages
classbytes/objbytes/spanobjectstail wastemax waste
1881921024087.50%
2168192512043.75%
3248192341029.24%
4328192256046.88%
54881921703231.52%
6648192128023.44%
78081921023219.07%
6732768327681012.50%
runtime.mspan

((4833)170+32)/8192=0.31518((48−33)∗170+32)/8192=0.31518((48−33)∗170+32)/8192=0.31518

除了上述 67 个跨度类之外,运行时中还包含 ID 为 0 的特殊跨度类,它能够管理大于 32KB 的特殊对象。

1.2 线程缓存(mcache)

runtime.mcachenumSpanClassesmspanmcachealloc
//go:notinheap
type mcache struct {
   ...

   alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass

   ...
}

其图示如下:

图片来源于链接,侵删!

1.3 中心缓存(mcentral)

runtime.spanSet
//go:notinheap
type mcentral struct {
   spanclass spanClass
   partial [2]spanSet // list of spans with a free object
   full    [2]spanSet // list of spans with no free objects
}

如图上所示,是 runtime.mcentral 中的 spanSet 的内存结构,index 字段是一个uint64类型数字的地址,该uint64的数字按32位分为前后两半部分head和tail,向spanSet中插入和获取mspan有其提供的push和pop函数,以push函数为例,会根据index的head,对spanSetBlock数据块包含的mspan的个数512取商,得到spanSetBlock数据块所在的地址,然后head对512取余,得到要插入的mspan在该spanSetBlock数据块的具体地址。之所以是512,因为spanSet指向的spanSetBlock数据块是一个包含512个mspan的集合。

spanClassruntime.mcentral

1.4 页堆(mheap)

//go:notinheap
type mheap struct {
   ...
   arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
   ...
   central [numSpanClasses]struct {
      mcentral mcentral
      pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
   }
   ...
}
runtime.mheap
mheap_
var mheap_ mheap
numSpanClassesruntime.mcentralscannoscan

arenas是heapArena的二维数组的集合。如下:

2. 内存分配

runtime.newobject
  • 微对象(0, 16B):先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;多个小于16B的无指针微对象的内存分配请求,会合并向Tiny微对象空间申请,微对象的 16B 内存空间从 spanClass 为 4 或 5(无GC扫描)的mspan中获取。
  • 小对象[16B, 32KB]:先向mcache申请,mcache内存空间不够时,向mcentral申请,mcentral不够,则向页堆mheap申请,再不够就向操作系统申请。
  • 大对象(32KB, +∞):大对象直接向页堆mheap申请。

对于内存的释放,遵循逐级释放的策略。当ThreadCache的缓存充足或者过多时,则会将内存退还给CentralCache。当CentralCache内存过多或者充足,则将低命中内存块退还PageHeap。

3. 参考文献