一文搞懂Go1。20内存分配器
作者:mingguangtu,腾讯IEG后台开发工程师
导语最近学习了Go内存分配器的相关文章,结合Go最新的源码,以简单精炼的语言和丰富的图表形式,输出了自己的Go内存分配器的学习笔记。
关于Go内存分配器的分析文章很多,看到的比较经典的有刘丹冰Aceld的一站式Golang内存管理洗髓经,最近学习了该篇文章和其他相关文章,结合Go1。20最新的源码,复习了下Go内存分配的知识,输出了自己的学习笔记。要学习GoGC实现,需要先搞定内存分配,内存分配是GC垃圾回收的前传。结论
1。TCMalloc内存分配的核心思想是:多级缓存机制,每个线程Thread自行维护一个无锁的线程本地缓存ThreadCache,小对象优先从本地缓存申请内存,内存不足时向加锁的中央缓存CentralCache申请,中央缓存不足时向页堆PageHeap申请,大对象直接向页堆PageHeap申请。
2。Go内存分配器的设计思想主要来自TCMalloc,也是包含三级缓存机制:每个线程M所在的P维护一个无锁的线程本地缓存mcache,内存不足时向对应spanClass规格的中央缓存mcentral加锁申请资源,中央缓存不足时向页堆mheap申请,大对象直接向页堆mheap申请。
3。Go内存分配器与操作系统虚拟内存交互的最小单元是Page,即虚拟内存页;多个连续的Page称为一个mspan,mspan是Go内存分配的基本单元;每个mspan有个字段叫spanClass跨度类,是对mspan大小级别的划分,每个mspan能够存放指定范围大小的对象,32KB以内的小对象在Go中,会对应不同大小的内存刻度SizeClass,SizeClass和ObjectSize是一一对应的,前者指序号0、1、2、3,后者指具体对象大小0B、8B、16B、24B;每一个SizeClass根据是否有指针,对应两个spanClass规格的mspan;每个mspan都会有特定的SizeClass内存规格、spanClass跨度规格、存储特定ObjectSize的对象,并且包含特定数量的页数Pages。
4。Go中,mcache线程缓存负责微对象和小对象(32KB)的分配;弄清楚了mspan的内存结构,就很容易理解mcache,mcache只是包含了全部spanClass规格的mspan的一个内存结构,绑定在本地P上,访问无需加锁。和mcache不一样,每个spanClass规格对应一个mcentral中央缓存,全部spanClass规格的mcentral共同构成了中央缓存层MCentral,访问需要加锁;mcentral主要包含partialSet空闲集合和fullSet非空闲集合,支持并发高效存取mspan。mheap主要由多个64M的heapArena组成,每个heapArena主要包含8192页的一个列表,每页分别对应一个mspan。
5。微对象的分配逻辑是:多个小于16B的无指针微对象的内存分配请求,会合并向Tiny微对象空间申请,微对象的16B内存空间从spanClass为4或5(无GC扫描)的mspan中获取。
6。小对象的分配逻辑是:先向mcache申请,mcache内存空间不够时,向mcentral申请,mcentral不够,则向页堆mheap申请,再不够就向操作系统申请。大对象直接向页堆mheap申请。1。TCMalloc内存分配思想
Go语言的内存分配器采用了TCMalloc多级缓存分配思想来构建的。
如图1。1所示,是TCMalloc内存分配器的主要模块。根据TCMalloc:ThreadCachingMalloc的描述,TCMalloc为每个线程Thread分配一个线程本地缓存。线程本地缓存满足小对象(32KB)的分配。每个线程Thread在申请内存时首先会从这个线程缓存ThreadCache申请,所有线程缓存ThreadCache共享一个中央缓存CentralCache。当线程缓存不足时,ThreadCache会向中央缓存CentralCache申请内存,当中央缓存CentralCache内存不足时,会向页堆PageHeap申请,页堆内存不足,会向操作系统的虚拟内存申请。
图1。1TCMalloc内存分配模型
线程对于大对象(32KB)的分配是直接向页堆PageHeap申请,不经过线程缓存ThreadCache和中央缓存CentralCache。
CentralCache由于共享,它的访问是需要加锁的。ThreadCache作为线程独立的第一交互内存,访问无需加锁。CentralCache则作为ThreadCache临时补充缓存。PageHeap也是一次系统调用从虚拟内存中申请的,PageHeap明显是全局的,其访问一定要加锁。
对于内存的释放,遵循逐级释放的策略。当ThreadCache的缓存充足或者过多时,则会将内存退还给CentralCache。当CentralCache内存过多或者充足,则将低命中内存块退还PageHeap。
总之,TCMalloc的核心原理是:把内存分为多级管理,从而降低锁的粒度。每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。2。Go内存分配相关概念
Go内存分配器的设计思想主要来自TCMalloc,接下来详细分析一下Go内存管理模型的相关概念和核心模块。如图2。1所示,是Go的内存管理模型。
图2。1Go内存管理模型3。线程缓存mcache
要分析Go内存模型,需要先弄清楚内存分配相关的几个基本概念,有Page,mspan,SizeClass等。3。1虚拟内存页Page
Page,也叫虚拟内存页,表示Go内存管理与操作系统虚拟内存交互内存的最小单元。一个Page的大小是8KB。操作系统虚拟内存对于Go来说,是划分成等分的N个Page组成的一大块公共内存池。
图3。1虚拟内存是N个Page组成的一大块公共内存池3。2内存管理单元mspan
多个连续的Page称为一个mspan,mspan是Go内存管理的基本单元。Go内存管理组件是以mspan为单位向操作系统虚拟申请内存的。每个mspan记录了第一个起始Page的地址startAddr,和一共有多少个连续Page的数量npages。
为了方便mspan和mspan之间的管理,mspan集合是以双向链表的形式构建。
图3。2mspan代表一块连续的Page
runtime。mspan的源码如下:srcruntimemheap。gotypemspanstruct{sys。NotInHeapnextmspan下一个span节点prevmspan上一个span节点startAddruintptrspan开始地址npagesuintptrspan包含的页数freeindexuintptr空闲对象的索引nelemsuintptrspan中存放的对象数量allocCacheuint64allocBits的补码,可以用于快速查找内存中未被使用的内存allocBitsgcBits标记内存的占用gcmarkBitsgcBits标记内存的GC回收情况spanclassspanClassspanClass规格和是否GC扫描statemSpanStateBox标记mspan的状态,状态可能处于mSpanDead、mSpanInUse、mSpanManual和mSpanFree四种情况,在空闲堆或被分配或处于GC不同阶段,会有不同状态}
runtime。mspan的主要字段有:
next和prev两个字段,它们分别指向了前一个和后一个runtime。mspan;
startAddr和npages分别代表mspan管理的堆页的起始地址和数量;
freeindex和nelems分别表示空闲对象的索引和这个mspan中存放的对象数量;
allocBits和gcmarkBits分别用于标记内存的占用和GC回收情况;
allocCache是allocBits的补码,用于快速查找内存中未被使用的内存;
spanclass表示mspan所属的大小规格和GC扫描信息,每个mspan都有不同的大小规格,存放小于32KB的不同大小的对象;就像下一小节说的,一个SizeClass对应两个spanClass,其中一个Span为存放需要GC扫描的对象(包含指针的对象),另一个Span为存放不需要GC扫描的对象(不包含指针的对象);
state标记mspan的状态,状态可能处于mSpanDead、mSpanInUse、mSpanManual和mSpanFree四种情况,在空闲堆或被分配或处于GC不同阶段,会有不同状态。3。3spanClass、SizeClass和ObjectClass
mspan有个字段spanClass,是跨度类,是对mspan大小级别的划分。
1)提到跨度类spanClass,就不得不提内存刻度进行衡量的SizeClass。Go对于32KB以内的小对象,会将这些小对象按不同大小划分为多个内存刻度SizeClass,是不同大小对象ObjectSize按顺序排序的序号,如0,1,2,3。每个SizeClass都对应一个对象大小即ObjectSize,如8B、16B、32B等。在申请小对象内存时,Go会根据使用方申请的对象大小,就近向上取最接近的一个ObjectSize,找到其所在的序号SizeClass,和所代表的spanClass跨度类的mspan。
Go内存管理模块中一共包含68中内存刻度SizeClass,每一个SizeClass都会存储特定大小即ObjectSize的对象,并且包含特定数量的页数npages,SizeClass和ObjectSize及页数npages的关系,存储在runtime。classtosize和runtime。classtoallocnpages等变量中:const(。。。NumSizeClasses68。。。)varclasstosize〔NumSizeClasses〕uint16{0,8,16,24,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,288,320,352,384,416,448,480,512,576,640,704,768,896,1024,1152,1280,1408,1536,1792,2048,2304,2688,3072,3200,3456,4096,4864,5376,6144,6528,6784,6912,8192,9472,9728,10240,10880,12288,13568,14336,16384,18432,19072,20480,21760,24576,27264,28672,32768}varclasstoallocnpages〔NumSizeClasses〕uint8{0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,3,2,3,1,3,2,3,4,5,6,1,7,6,5,4,3,5,7,2,9,7,5,8,3,10,7,4}
2)在Go中,spanClass和SizeClass的关系是,一个SizeClass对应两个spanClass,其中一个Span为存放需要GC扫描的对象(包含指针的对象),另一个Span为存放不需要GC扫描的对象(不包含指针的对象)。
图3。3Go中一个SizeClass对应两个spanClass
如图3。3所示,不同对象占用内存大小不同,会申请不同规格的mspan来存放,比如一个对象的大小是14B(向上取最靠近的Object大小16B),所属的mspan大小是8KB(一页Page),则这个mspan会被平均分割为512个Object(819216512),当Go程序向Go内存分配器申请内存,实际上是分配该规格SizeClass2(spanClass4或5)的mspan的一个对象大小的空间出去,剩下5121511个对象空间待分配。这里,SizeClass和ObjectSize是一一对应的,只不过一个是序号,一个是具体对象大小。
SizeClass和spanClass的对应关系参考如下源码:srcruntimemheap。gotypespanClassuint8spanClass的个数const(spanClass的个数是SizeClass个数的两倍numSpanClassesNumSizeClasses1)通过SizeClass来得到对应的SpanClass,第二个形参noscan表示当前对象是否需要GC扫描funcmakeSpanClass(sizeclassuint8,noscanbool)spanClass{returnspanClass(sizeclass1)spanClass(bool2int(noscan))}
对makeSpanClass()函数的逻辑解释如下表所示:
mspan类型
SizeClass与spanClass对应公式
需要GC扫描
spanClassSizeClass20
不需要GC扫描
spanClassSizeClass21
通过Go的源码,可以知道Go内存池固定划分了67个SizeClass(算上0是68个),并列举了详细的SizeClass和Object大小、存放Object数量,以及每个SizeClass对应的Span内存大小关系:srcruntimesizeclasses。go标题Title解释:〔class〕:SizeClass〔bytesobj〕:ObjectSize,一次对外提供内存Object的大小〔bytesspan〕:当前Object所对应Span的内存大小〔objects〕:当前Span一共能存放多少个Object〔tailwaste〕:为当前Span平均分层N份Object,会有多少内存浪费〔maxwaste〕:当前SizeClass最大可能浪费的空间所占百分比classbytesobjbytesspanobjectstailwastemaxwasteminalign1881921024087。5082168192512043。75163248192341829。2484328192256021。883254881921703231。52166648192128023。446478081921023219。07168968192853215。9532351408163841189614。001283615368192551214。0051237179216384925615。5725638204881924012。452048。。。652726481920312810。00128662867257344204。9140966732768327681012。508192
每列的含义是:
1)class:SizeClass规格;
2)bytesobj:ObjectSize,一次对外提供内存Object的大小;
3)bytesspan:当前Object所对应Span的内存大小;
4)objects:当前Span一共能存放多少个Object;
5)tailwaste:为当前Span平均分层N份Object,会有多少内存浪费;
6)maxwaste:当前SizeClass最大可能浪费的空间所占百分比。
以SizeClass为35的一行为例,ObjectSize是1408B,对应的mspan的大小是16KB,占2Page,能存放的Object个数是16KB1408B11。636,向下取整,该mspan最多能存11个1408B大小的对象。
如果存的对象大小就是1408B,尾部会浪费16KB1408B11896B,即是tailwaste尾部浪费列的值。
如果存放的对象是该ObjectSize为1408B的档位能存放的最小的对象,即上一档位的ObjectSize1128011281B,则该mspan会产生最大浪费,比例是〔(14081281)11896〕B16KB22931638414,即是maxwaste最大浪费列的值。4。线程缓存mcache
将runtime。mspan的内存结构剖析清楚了,再理解runtime。mcache就会非常简单。
如图4。1所示,runtime。mcache是与Go协程调度模型GPM中的P所绑定,而不是和线程M绑定,因为在Go调度的GPM模型中,真正可运行的线程M的数量与P的数量一致,即GOMAXPROCS个,跟P绑定节省了P移动到其他M上去的mcahe的切换开销。每个G使用MCache时不需要加锁就可以获取到内存。
图4。1runtime。mcache与P绑定
runtime。mcache的源码是:srcruntimemcache。gotypemcachestruct{分配tiny对象的参数tinyuintptr申请tiny对象的起始地址tinyoffsetuintptr从tiny地址开始的偏移量tinyAllocsuintptrtiny对象分配的数量alloc〔numSpanClasses〕mspan待分配的mspan列表,通过spanClass索引}
runtime。mcache的字段主要包含两部分:
tiny,tinyoffset,tinyAllocs是跟tiny微对象分配相关的参数;
alloc是待分配的mspan列表,不同规格的mspan通过spanClass值索引。
如图4。2所示,是runtime。mcache的内存结构,主要包含两部分的内容,Tiny对象的分配空间,和alloc列表代表的对于小对象的分配空间,其实是由0到135(2821)个spanClass规格大小的mspan组成的列表。
图4。2mcache内存结构
每一个线程缓存runtime。mcache都持有682个runtime。mspan,这些内存管理单元都存储在结构体的alloc字段中。5。中心缓存mcentral
当mcache中某个SizeClass对应的Span的一个个Object被应用分配走后,如果出现当前SizeClass的mspan空缺情况,mcache则会向mcentral申请对应的mspan。
图5。1mcache在内存不足时向中心缓存mcentral申请资源
runtime。mcentral的源码如下:srcruntimemcentral。go给定SizeClass规格的中央缓存mcentraltypemcentralstruct{该mcentral的spanClass规格大小spanclassspanClasspartial〔2〕spanSet维护全部空闲的span集合,partial有两个spanSet集合,其中一个是GC已经扫描的,一个是GC未扫描的full〔2〕spanSet维护已经被使用的span集合,full也有两个spanSet集合,一个GC已扫描,一个未扫描}
runtime。mcentral有个字段spanclass,代表这个mcentral的类型,不同的SizeClass规格的mspan对应有不同的runtime。mcentral管理。SizeClass总共有0,8B,16B,24B,32B到32KB共68种,因此Go内存分配器的中央缓存模型MCentral总共有排除掉0的67种runtime。mcentral。
partial和full分别维护空闲的mspan集合,和已经被使用的mspan集合。mcache向mcentral申请资源,当然是从partial集合获取。partial和full都是一个〔2〕spanSet类型,也就每个partial和full都各有两个spanSet集合,这是为了给GC垃圾回收来使用的,其中一个集合是已扫描的,另一个集合是未扫描的。
spanSet简单理解是mspan的集合,详细理解需要看它的源码:srcruntimemspanset。gospanSet是并发安全的存取mspan的集合,本质是个二级数据结构,一级是index字段存储指向spanSetBlock的索引,第二级是spanSetBlock存储指向512个mspan数组的索引typespanSetstruct{spineLockmutex锁,往spanSet放入push和获取popmspan需要加锁spineatomicSpanSetSpinePointer指向spanSetBlock集合的地址spineLenatomic。UintptrspanSetBlock集合的lengthspineCapuintptrspanSetBlock集合的capindexatomicHeadTailIndex指向spanSetBlock集合的头尾指针组成的一个int64位的数字}
spanSet是并发安全的存取mspan的集合,实际上是个二级数据结构,一级是index字段存储指向spanSetBlock数据块的索引,第二级是spanSetBlock存储指向512个mspan数组的索引。
图5。2mcentral中的spanSet内存结构
如图5。2所示,是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的集合。
如图5。3所示,是中央缓存模型MCentral的内存结构,是由全部spanClass规格的runtime。mcentral共同组成。
图5。3MCentral中央缓存模型,由全部spanClass规格的runtime。mcentral共同组成6。页堆mheap
先看runtime。mheap的源码:srcruntimemheap。go页堆mheaptypemheapstruct{lockmutexmheap的锁pagespageAlloc页分配数据结构allspans〔〕mspan所有的spans都是通过mheap申请,所有申请过的mspan都会记录在allspans,可以随着堆的增长重新分配和移动heapArena二维数组集合arenas〔1arenaL1Bits〕〔1arenaL2Bits〕heapArena。。。arenas的地址集合,用来管理heapArena的增加arenaHintsarenaHint。。。全部规格的mcental集合,中央缓存MCentral本身也是MHeap的一部分central〔numSpanClasses〕struct{mcentralmcentralpad〔(cpu。CacheLinePadSizeunsafe。Sizeof(mcentral{})cpu。CacheLinePadSize)cpu。CacheLinePadSize〕byte}各种分配器spanallocfixallocspan分配器cacheallocfixallocmcache分配器specialfinalizerallocfixallocspecialfinalizer分配器specialprofileallocfixallocspecialprofile分配器specialReachableAllocfixallocspecialReachable分配器speciallockmutex特殊记录分配器的锁arenaHintAllocfixallocallocatorforarenaHints。。。}
runtime。mheap页堆主要包含两种数据结构arenas和central:
arenas是heapArena的二维数组的集合;
central是全部规格的中央缓存runtime。central的集合,重要缓存层MCentral本身也是页堆mheap的一部分。
heapArena类型,用来存储heaparena元数据:const(pageSize81921138KheapArenaBytes67108864一个heapArena是64MBheapArenaWordsheapArenaBytes864位的Linux系统,一个heapArena有8M个word,一个word占8个字节heapArenaBitmapWordsheapArenaWords一个heapArena的bitmap占用8M64131072,即128KpagesPerArenaheapArenaBytespageSize一个heapArena包含8192个页)typeheapArenastruct{bitmap中每个bit标记arena中的一个wordbitmap〔heapArenaBitmapWords〕uintptrbitmap的8个比特表示一个字长,这个字长是否包含指针用下面的数组记录noMorePtrs〔heapArenaBitmapWords8〕uint8记录当前arena中每一页对应到哪一个mspanspans〔pagesPerArena〕mspan位图类型,标记哪些spans处于mSpanInUse状态pageInUse〔pagesPerArena8〕uint8位图类型,记录哪些spans已被标记pageMarks〔pagesPerArena8〕uint8位图类型,指哪些spans有specials(finalizersorother)pageSpecials〔pagesPerArena8〕uint8零基地址,标记arena页中首个未被使用的页的地址zeroedBaseuintptr}
Go1。20的runtime。heapArena和Go1。18之前的有不同的是bitmap的一个比特位表示一个word字,而不是一个字节Byte,在Linux64系统中,一个heapArena管理的内存大小是64MB,那么用一个比特位指代一个字长,则需要64MB(864)128K个Bit,而且一个占有8Bword的对象是否有指针是用另一个字段noMorePtrs来记录的,而不是都在bitmap中体现。
runtime。heapArena的最重要的字段依然是spans,表示一页arena对应的是哪个mspan。
如图6。1所示,是runtime。mheap的内存结构。
图6。1mheap内存结构7。微、小、大对象分配过程
Go中根据对象大小分为三种对象类型:
对象类型
对象大小
tiny微对象
16B
small小对象
〔16B,32KB〕
large大对象
32KB
不同大小的对象有不同的内存分配机制。7。1微对象的分配过程
Go将小于16字节的对象划分为微对象,它会使用线程缓存上的微分配器Tinyallocator提高微对象分配的性能,我们主要使用它来分配较小的字符串以及逃逸的临时变量。微分配器可以将多个较小的内存分配请求合并存入同一个内存块中,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收。
微分配器管理的对象不可以是指针类型,管理多个对象的内存块大小maxTinySize是可以调整的,在默认情况下,内存块的大小为16字节。maxTinySize的值越大,组合多个对象的可能性就越高,内存浪费也就越严重;maxTinySize越小,内存浪费就会越少,不过无论如何调整,8的倍数都是一个很好的选择。
如图7。1所示,微对象的16B内存空间从spanClass为4或5(无GC扫描)的mspan中获取,因为spanClass为2或3的mspan的Object大小是8B,而不是16B。
图7。1微对象的16B内存空间从spanClass为4或5(无GC扫描)的mspan中获取
如图7。2所示,是以bool变量申请1B内存为例说明的Tiny微对象的分配过程。
图7。2微对象分配过程
微对象分配的具体源码如下:srcruntimemalloc。go堆上所有的对象都会通过调用runtime。newobject函数分配内存,该函数会调用runtime。mallocgc()函数根据对象的大小(字节数)来分配内存,微对象、小对象从每个P的mcache本地缓存分配,大对象从堆分配funcmallocgc(sizeuintptr,typtype,needzerobool)unsafe。Pointer{。。。。。。获取当前G所在的M线程mp:acquirem()。。。。。获取M的mcache本地缓存c:getMCache(mp)ifcnil{throw(mallocgccalledwithoutaPoroutsidebootstrapping)}varspanmspanvarxunsafe。Pointernoscan变量记录对象是否包含指针,true为不包含noscan:typniltyp。ptrdata0ifsizemaxSmallSize{size是对象的字节数,如果对象小于32KBifnoscansizemaxTinySize{如果对象小于16B且不包含指针这里有一段注释,说明微分配器Tinyallocator的内存空间大小为何是16B。Tiny内存空间越大,如32B,小对象(小字符串、数字、布尔等)组合的可能性越大,浪费也越严重,综合考虑,16B是合适的,一组jsonbenchmark测试显示,通过Tiny分配器,可以减少12的分配次数和20的堆大小;获取mcache的变量tinyoffset,表示Tiny空间空闲的位移off:c。tinyoffset内存对齐ifsize70{size在(0,16)范围,如果低三位是0,则跟8位对齐offalignUp(off,8)}elseifgoarch。PtrSize4size12{地址长度是4,字节数是12,也是跟8位对齐offalignUp(off,8)}elseifsize30{size低二位是0,跟4位对齐offalignUp(off,4)}elseifsize10{size低一位是0,跟2位对齐offalignUp(off,2)}ifoffsizemaxTinySizec。tiny!0{如果对象的大小和off偏移量加起来仍然比16B小,且Tiny空间存在,则当前对象可以从已有的Tiny空间分配在当前的Tiny空间分配,增加相关变量大小xunsafe。Pointer(c。tinyoff)c。tinyoffsetoffsizec。tinyAllocsmp。mallocing0releasem(mp)returnx分配后直接返回}从当前P的mcache的spanClass为4或5的mspan中,申请一个新的16BObject的Tiny空间spanc。alloc〔tinySpanClass〕v:nextFreeFast(span)ifv0{v,span,shouldhelpgcc。nextFree(tinySpanClass)}xunsafe。Pointer(v)将申请的16B内存空间置0,16B由两个长度为uint64地址指向(〔2〕uint64)(x)〔0〕0(〔2〕uint64)(x)〔1〕0非竞态下,如果申请的内存块分配了新对象后,将剩下的给tiny,用tinyoffset记录分配了多少if!raceenabled(sizec。tinyoffsetc。tiny0){c。tinyuintptr(x)c。tinyoffsetsize}sizemaxTinySize}else{小对象分配逻辑。。。。。}}else{大对象分配逻辑。。。。。。}。。。。。。}7。2小对象的分配过程
对于对象在16B至32B的内存分配,Go会采用小对象的分配流程。
图7。3小对象内存分配流程
如图7。3所示是小对象的内存分配流程:
1)首先是Goroutine用户程序向Go内存管理模块申请一个对象所需的内存空间;
2)Go内存管理模块的runtime。mallocgc()函数检查传入的对象大小是否大于32KB,如果是则进入大对象分配逻辑,不是则接着往下走;
3)如果对象小于32KB,接着判断对象是否小于16B,如果是,则进入Tiny微对象分配逻辑,否则进入小对象分配逻辑;
4)根据对象的字节数Size匹配得到对应的SizeClass内存规格,再根据SizeClass和对象是否包含指针得到spanClass,从mcache的对应spanClass的mspan中获取所需内存空间;
5)在找到的mcache的对应spanClass的mspan中如果有空闲的内存空间,则直接获取并返回;
6)如果定位的mcache的对应spanClass的mspan中没有空闲内存空间,则mcache会向MCentral中央缓存层对应spanClass的mcental申请一个mspan;
7)MCentral层收到mcache的内存申请请求后,优先从相对应的spanClass的partialSet空闲集合中取出mspan,partialSet没有则从fullSet非空闲集合中获取,取到了则返回给mcache;
8)mcache得到了mcentral返回的mspan,补充到自己对应的spanClass列表中,返回第5)步,获取内存空间,结束流程;
9)如果mcentral的partialSet和fullSet都没有符合条件的mspan,则MCentral层会向MHeap页堆申请内存;
10)MHeap收到内存请求从其中一个heapArena从取出一部分Pages返回给MCentral,当MHeap没有足够的内存时,MHeap会向操作系统申请内存,将申请的内存也保存到heapArena中的mspan中。MCentral将从MHeap获取的由Pages组成的mspan添加到对应的spanClass集合中,作为新的补充,之后再次执行第(7)步。
11)最后,Goroutine用户程序获取了对象所需内存,流程结束。
runtime。mallocgc()函数中关于小对象分配的源码如下:Allocateanobjectofsizebytes。SmallobjectsareallocatedfromtheperPcachesfreelists。Largeobjects(32kB)areallocatedstraightfromtheheap。funcmallocgc(sizeuintptr,typtype,needzerobool)unsafe。Pointer{。。。。。。获取线程Mmp:acquirem()。。。。。获取G所在P的mcachec:getMCache(mp)。。。。。。对象是否包含指针noscan:typniltyp。ptrdata0。。。。。。ifsizemaxSmallSize{对象小于32KBifnoscansizemaxTinySize{对象小于16B。。。。。。}else{对象在(16B,32KB),进入小对象分配流程varsizeclassuint8根据对象大小获取对象所属的SizeClassifsizesmallSizeMax8{sizeclasssizetoclass8〔pRoundUp(size,smallSizeDiv)〕}else{sizeclasssizetoclass128〔pRoundUp(sizesmallSizeMax,largeSizeDiv)〕}sizeuintptr(classtosize〔sizeclass〕)根据SizeClass和对象是否包含指针,获取所属的spanClassspc:makeSpanClass(sizeclass,noscan)从mcache中获取对应spanClass的mspanspanc。alloc〔spc〕从mpsan分配一个空闲的Objectv:nextFreeFast(span)ifv0{mcache没有空闲的Object,则从mcentral申请可用的mspan给到mcachev,span,shouldhelpgcc。nextFree(spc)}这时,mcache中有了空闲的Object空间,获取并返回xunsafe。Pointer(v)ifneedzerospan。needzero!0{memclrNoHeapPointers(x,size)}}}else{大对象分配流程。。。。。。}。。。。。。returnx}
注释中整体的流程对应前面的流程图,需要继续补充说明的是mcache。nextFree()函数,其作用是在mcache的mspan没有空闲的Object时,会通过该函数从mcentral获取:srcruntimemalloc。go从当前的mspan获取下一个空闲的Object空间,如果没有则从mcentral获取,否则从mheap获取func(cmcache)nextFree(spcspanClass)(vgclinkptr,smspan,shouldhelpgcbool){sc。alloc〔spc〕shouldhelpgcfalse当前mspan找到空闲的Object空间的index索引freeIndex:s。nextFreeIndex()iffreeIndexs。nelems{如果mspan已满Thespanisfull。ifuintptr(s。allocCount)!s。nelems{println(runtime:s。allocCount,s。allocCount,s。nelems,s。nelems)throw(s。allocCount!s。nelemsfreeIndexs。nelems)}从mcentral获取可用的mspan,并替换当前的mcache的mspanc。refill(spc)shouldhelpgctruesc。alloc〔spc〕再次到新的mspan里查找空闲的Object索引freeIndexs。nextFreeIndex()}iffreeIndexs。nelems{throw(freeIndexisnotvalid)}计算要使用的Object内存块在mspan中的地址,并返回该mspanvgclinkptr(freeIndexs。elemsizes。base())s。allocCountifuintptr(s。allocCount)s。nelems{println(s。allocCount,s。allocCount,s。nelems,s。nelems)throw(s。allocCounts。nelems)}return}
mcache。nextFree()函数会检查当前mspan是否有空闲的Object内存块,如果满了就调用mcache。refill()方法从mcentral中获取可用的mspan,并替换掉当前mcache里面的mspan。至于mcache。refill()方法的具体逻辑,这里不再赘述。7。3大对象的分配过程
从runtime。mallocgc()函数可以看到,大对象的内存分配主要通过调用mcache。allocLarge()函数实现:funcmallocgc(sizeuintptr,typtype,needzerobool)unsafe。Pointer{。。。。。。获取当前mcachec:getMCache(mp)。。。。。。对象是否包含指针noscan:typniltyp。ptrdata0。。。。。。ifsizemaxSmallSize{对象小于32KBifnoscansizemaxTinySize{。。。。。。}else{。。。。。。}}else{大对象内存分配逻辑shouldhelpgctrue调用mcache。allocLarge()根据对象的大小size和是否包含指针分配大内存mspanspanc。allocLarge(size,noscan)span。freeindex1span。allocCount1sizespan。elemsizexunsafe。Pointer(span。base())。。。。。。}。。。。。。}
mcache。allocLarge()函数主要根据对象的大小获取要分配的页数npages,然后调用mheap。alloc()函数从mheap中直接分配npages页的mspan:srcruntimemcache。goallocLarge()函数为大对象申请一个mspanfunc(cmcache)allocLarge(sizeuintptr,noscanbool)mspan{Linux64位系统下PageSize为8K,如果对象太大发生溢出,甩出错误ifsizePageSizesize{throw(outofmemory)}PageShift13,2的13次方是8192,用对象大小size8192得到需要分配的页数npagesnpages:sizePageShift页数npages不是整数,多出来一些小数,加1ifsizePageMask!0{npages}。。。。。。从mheap页堆上分配npages页SizeClass为0的mspan,spanClass根据对象是否包含指针为0或1spc:makeSpanClass(0,noscan)s:mheap。alloc(npages,spc)ifsnil{throw(outofmemory)}。。。。。。returns}
mheap。alloc()函数主要在系统栈上调用mheap。allocSpan()获取具体的npages页mspan:srcruntimemheap。goalloc()函数从GC的堆上分配npages页的mspanfunc(hmheap)alloc(npagesuintptr,spanclassspanClass)mspan{varsmspansystemstack(func(){为了阻止额外的堆增长,如果GC扫描未结束,在分配前需要先回收npages个页的内存if!isSweepDone(){h。reclaim(npages)}调用mheap。allocSpan()获取具体的npages页mspansh。allocSpan(npages,spanAllocHeap,spanclass)})returns}
mheap。allocSpan()是获取大对象所需内存页的具体实现:srcruntimemheap。gofunc(hmheap)allocSpan(npagesuintptr,typspanAllocType,spanclassspanClass)(smspan){获取当前Ggp:getg()base,scav:uintptr(0),uintptr(0)growth:uintptr(0)在某些平台上,需要进行物理页对齐needPhysPageAlign:physPageAlignedStackstypspanAllocStackpageSizephysPageSize当对象足够小,且无需进行物理页对齐,优先从本地P的缓存分配内存pp:gp。m。p。ptr()if!needPhysPageAlignpp!nilnpagespageCachePages4{c:pp。pcache本地P的缓存为空,从mheap分配缓存pageCache给它ifc。empty(){lock(h。lock)ch。pages。allocToCache()unlock(h。lock)}从本地P的缓存pageCache获取npages页内存base,scavc。alloc(npages)ifbase!0{sh。tryAllocMSpan()ifs!nil{获取到了内存,调整到HaveSpan位置gotoHaveSpan}}}lock(h。lock)ifneedPhysPageAlign{需要物理页对齐进行物理页对齐需要增加的额外页数extraPages:physPageSizepageSize从mheap页堆的pages页分配数据结构中查找所需物理对齐后的页数base,h。pages。find(npagesextraPages)ifbase0{如果没有获取到,调用mheap。grow()函数增长页堆varokboolgrowth,okh。grow(npagesextraPages)if!ok{unlock(h。lock)returnnil}base,h。pages。find(npagesextraPages)ifbase0{throw(grewheap,butnoadequatefreespacefound)}}将获取的内存地址base进行物理页对齐,并获取内存大小scavbasealignUp(base,physPageSize)scavh。pages。allocRange(base,npages)}ifbase0{如果在上面两种情况下,依然没有获取到所需内存页从mheap的pages页分配数据结构中获取base,scavh。pages。alloc(npages)ifbase0{内存不够,调用mheap。grow()扩容varokboolgrowth,okh。grow(npages)if!ok{unlock(h。lock)returnnil}重新从mheap的pages页分配数据结构中获取内存base,scavh。pages。alloc(npages)内存还是不足,则抛出异常ifbase0{throw(grewheap,butnoadequatefreespacefound)}}}ifsnil{分配一个mspan对象sh。allocMSpanLocked()}unlock(h。lock)HaveSpan:。。。。。初始化得到的mspan,并将其与mheap关联起来h。initSpan(s,typ,spanclass,base,npages)。。。。。returns}
mheap。allocSpan()函数的主要逻辑是:
1)根据平台差异,检查对象所需内存是否需要进行物理页对齐;
2)当无需进行物理页对齐,且对象足够小,即小于pageCachePages464416页时,优先从本地P的缓存分配内存;此时,如果本地P的缓存为空,从mheap分配缓存pageCache给它,然后再获取所需内存;
3)当需要物理页对齐,从mheap页堆的pages页分配数据结构中查找(mheap。pages。find函数)所需物理对齐后的页数,如果没有获取到,调用mheap。grow()函数扩容mheap,之后再获取;
4)如果在上面两种情况下,依然没有获取到所需内存页,则从mheap的pages页分配数据结构中获取(mheap。pages。alloc函数),如内存不够,则调用mheap。grow()扩容,之后,再重新从mheap的pages页分配数据结构中获取内存;
5)获取了所需内存页后,分配一个新的mspan,初始化相关参数,并将二者绑定,结束流程。8。总结
Go内存分配器中使用到的多级缓存机制,是程序开发中常用的设计理念,将对象根据大小分为不同规格,通过提高数据局部性和细粒度内存的复用率,能够有效提升不同大小对象的内存分配的整体效率。Reference
一站式Golang内存管理洗髓经https:www。yuque。comaceldgolangqzyivna7sjw
内存分配器https:draveness。megolangdocspart3runtimech07memorygolangmemoryallocatorE58685E5AD98E7AEA1E79086E58D95E58583
内存分配https:golang。designunderthehoodzhcnpart2runtimech07alloc
golang学习之路内存分配器https:xie。infoq。cnarticlee760c46349bd7b443e38ac332
详解Go中内存分配源码实现https:www。luozhiyun。comarchives434