0. 简介
程序中的数据都会被分配到程序所在的虚拟内存中,内存空间包括两个重要区域:栈(Stack) 和 堆(Heap)。函数调用的参数、返回值和局部变量大部分会分配在栈上,这部分由编译器办理。堆内存的办理方式视言语而定:
- C/C++等编程言语的堆内存由工程师主动恳求和释放;
- Go、Java等编程言语由工程师和编译器/运转时一起办理,其内存由内存分配器分配,由废物回收器回收。
本文就介绍一下Go言语的内存分配器。
1. Go内存分配规划原理
Go内存分配器的规划思维来源于TCMalloc
,全称是Thread-Caching Malloc
,核心思维是把内存分为多级办理,利用缓存的思维提升内存运用效率,下降锁的粒度。
图片来自链接,侵删!
如上图所示,是Go的内存办理模型示意图,在堆内存办理上分为三个内存级别:
- 线程缓存(MCache):作为线程独立的内存池,与线程的第一交互内存,拜访无需加锁;
- 中心缓存(MCentral):作为线程缓存的下一级,是多个线程同享的,所以拜访时需要加锁;
- 页堆(MHeap):中心缓存的下一级,在遇到32KB以上的目标时,会直接选择页堆分配大内存,而当页堆内存不行时,则会通过体系调用向体系恳求内存。
1.1 内存办理基本单元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.mspan
是Go内存办理的基本单元,其结构体中包括的next
和prev
指针,别离指向前后的runtime.mspan
,所以其串联后的结构是一个双向链表。
而startAddr
表明此mspan
的起始地址,npages
表明办理的页数,每页巨细8KB,这个页不是操作体系的内存页,一般是操作体系内存页的整数倍。
其它字段:
-
freeindex
— 扫描页中闲暇目标的初始索引; -
allocBits
和gcmarkBits
— 别离用于符号内存的占用和回收情况; -
allocCache
—allocBits
的补码,可以用于快速查找内存中未被运用的内存;
注意运用//go:notinheap
符号次结构体mspan
为非堆上类型,保证此类型目标不会逃逸到堆上。
图示:
跨度类
在mspan
中有一个字段是spanclass
,称为跨度类,是对mspan巨细级别的区分,每个mspan可以寄存指定规模巨细的目标,32KB以内的小目标在Go中,会对应不同巨细的内存刻度Size Class,Size Class和Object Size是一一对应的,前者指序号 0、1、2、3,后者指详细目标巨细 0B、8B、16B、24B
//go:notinheap
type mspan struct {
...
spanclass spanClass // size class and noscan (uint8)
...
}
Go 言语的内存办理模块中总共包括 67 种跨度类,每一个跨度类都会存储特定巨细的目标而且包括特定数量的页数以及目标,所有的数据都会被预选计算好并存储在runtime.class_to_size
和runtime.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% |
上表展现了目标巨细从 8B 到 32KB,总共 67 种跨度类的巨细、存储的目标数以及糟蹋的内存空间,以表中的第四个跨度类为例,跨度类为 5 的runtime.mspan
中目标的巨细上限为 48 字节、办理 1 个页、最多可以存储 170 个目标。由于内存需要按照页进行办理,所以在尾部会糟蹋 32 字节的内存,当页中存储的目标都是 33 字节时,最多会糟蹋 31.52% 的资源:
((48−33)∗170+32)/8192=0.31518((48−33)∗170+32)/8192=0.31518
除了上述 67 个跨度类之外,运转时中还包括 ID 为 0 的特殊跨度类,它可以办理大于 32KB 的特殊目标。
1.2 线程缓存(mcache)
runtime.mcache
是Go言语中的线程缓存,它会与线程上的处理器意义绑定,用于缓存用户程序恳求的细小目标。每一个线程缓存都持有numSpanClasses
个(68∗268*2)个mspan
,存储在mcache
的alloc
字段中:
//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的调集。
由全部spanClass
规格的runtime.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
是内存分配的核心结构体,其最重要的两个字段如上。
在Go中其被作为全局变量mheap_
存储:
var mheap_ mheap
页堆中包括一个长度为numSpanClasses
个(68∗268*2)个的runtime.mcentral
数组,其间 68 个为跨度类需要scan
的中心缓存,别的的 68 个是noscan
(没有指针,无需扫描)的中心缓存。
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. 参考文献
7.1 内存分配器
一文搞懂Go1.20内存分配器