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
| class | bytes/obj | bytes/span | objects | tail waste | max waste |
|---|---|---|---|---|---|
| 1 | 8 | 8192 | 1024 | 0 | 87.50% |
| 2 | 16 | 8192 | 512 | 0 | 43.75% |
| 3 | 24 | 8192 | 341 | 0 | 29.24% |
| 4 | 32 | 8192 | 256 | 0 | 46.88% |
| 5 | 48 | 8192 | 170 | 32 | 31.52% |
| 6 | 64 | 8192 | 128 | 0 | 23.44% |
| 7 | 80 | 8192 | 102 | 32 | 19.07% |
| … | … | … | … | … | … |
| 67 | 32768 | 32768 | 1 | 0 | 12.50% |
runtime.mspan
((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。