应用办公生活信息教育商业
投稿投诉
商业财经
汽车智能
教育国际
房产环球
信息数码
热点科技
生活手机
晨报新闻
办公软件
科学动态
应用生物
体育时事

一文搞懂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

索尼发布两款耳机舒适降噪LinkBudsS双芯降噪旗舰WH12022年5月19日,索尼(中国)有限公司发布两款重磅力作,舒适入耳降噪真无线耳机LinkBudsS和双芯驱动旗舰降噪耳机WH1000XM5,满足用户工作和生活多种需求,为聆听……专门喝高钙牛奶来补钙,有必要吗?柯大夫,你好。我家孩子正在发育期,每天都要喝一两盒牛奶。我看现在市面上高钙奶很畅销。请问有必要专门买高钙奶来补钙吗?另外,我家孩子稍微有些胖,用不用把全脂奶换成脱脂奶?……平野美宇大获全胜!43击败昔日国乒天才少女,韩国3项全军覆没北京时间6月26日,国际乒联斯洛文尼亚公开赛正在进行。目前,混双冠军已经出炉。国乒小将林诗栋蒯曼总比分3:1击败韩国选手金娜英赵大爆冷,成功夺冠。本站公开赛,蒯曼,林诗栋两人都……黑白灰早过时了!2022年流行色在这,照着穿紧跟潮流,美翻了大家在日常的搭配中,会觉得最为稳定和百搭的色系一定是会优选黑白灰的吧,对于绝大部分的女性而言,这三款基础色系在如今这个时尚浪潮的时代中,已经属于没有什么特色的存在了,也就是已经……亚冠14决赛综述全北现代30巴吞联直播吧8月22日讯今天进行的两场亚冠14决赛,全北现代和浦和红钻分别淘汰神户胜利船以及巴吞联,晋级本赛季亚冠四强。全北现代31神户胜利船上半场两队互交白卷,战成00……4款2699元高配低价手机,物美价廉,热销旗舰售价亲民很多年轻人入手智能手机,一般定位的是20003000元价位段,因为这个段位可以选择配置高、颜值好,又有一定质感的手机,而不像千元机外观那么粗糙了,下面我们来一起看看4款2699……赵本山徒弟现身葬礼,顶烈日吹唢呐,出场费20万,网友自降身价在7月18日,有网友在社交平台上分享了赵本山的徒弟程野参加葬礼的视频,引发了网友们的热议。在这个视频中,程野身穿白色的短袖搭配黑色裤子,脚上穿着小白鞋,头上戴着黑色的渔夫……这5个坏习惯,对你的身体健康有着很大的影响随着生活条件越来越好,大家也都逐渐意识到了拥有一个健康的身体才是重中之重。越来越多的人开始重新审视自己对于身材的定义,开始吃轻食、多运动。其实生活中一些常见的习惯,都有可……杜兰特这才是史上最佳球队,C位果然是他2012年的今天,美国梦十队卫冕奥运会冠军,这是杜兰特第一次体验到冠军的味道。回想起当年的一幕,他情不自禁地打开推特写道:出于对这项运动的尊重,我不会说这是历史最佳球队,该死,……吴亦凡表哥被强制执行六千万,起底家族资本内幕据企查查APP显示,近日,吴亦凡表哥吴林等新增被执行人信息,执行标的60175128元,关联案件为吴林、吴潇等与银行的金融借款合同纠纷。打开应用查看人渣茶座会:当李云迪与……25岁的狼队刺痛宝刀未老,登上巅峰赛第二名,不打比赛却依旧很在kpl联赛当中一共是有18支战队,每个赛季能够打首发的固定选手也就只有90人而已,而职业选手的数量却高达数千人甚至到万人,所以真正能够稳定在首发位置上的绝对是少数,那么这些人……日本开放旅游一个多月了,为什么7月的京都还是空无一人?日本恢复接纳外国游客入境已过去一个多月,但据日本出入国在留管理厅(ImmigrationServicesAgencyofJapa)的数据显示,6月10日至7月10日期间,日本总……
81101!深圳队爆出冷门,周鹏6中0,对方本土核心三分1110月17日19点35分,CBA常规赛,北控队挑战深圳队。开局之后,双方分差没有拉开。高登凭借着出色的表现,带动球队,建立优势。容子峰传球到正面,三分命中。哈斯2罚中1,廖三宁……戏水大巡游帐篷露营世界公园开启消夏模式北京日报客户端记者孙颖通讯员赵彤7200平方米的戏水乐园,融合美食、音乐、体育的露营活动,沙滩泳池幕布电影这个周末,世界公园开启了消夏模式。随着中小学生暑假的来临,……中国女排重新起航!21人名单出炉,蔡斌新模式将掀起一场青春风中国女排在东京奥运会创造了历史最差战绩,之后郎平辞职,朱婷、张常宁因伤退出,颜妮等人又因为年龄退役,使得中国女排陷入了谷底。不过随着老帅蔡斌重新拿起主教练的教鞭,中国女排又一次……LPL新赛季战队名单曝光,iG被评究极摆烂,三支战队获S级评随着S11赛季即将落幕,备受瞩目的2021英雄联盟LPL冬季转会期也已经在今日(12月13日)晚上7点59分彻底关闭。这意味着除了自由人以外,其他尚处在合同期内的选手都无法再进……温体效应为什么人体的温度在36。7度左右,而在夏季太阳光照射下的气温通常是22度到36度之间,人体为什么会觉得很热,甚至受不了比人体少了几度甚至是十几度的气温。按理来说,人的体温……来石景山西部这座森林公园,换个角度领略京西之美!在热闹喧嚣的城市中带您走进一个有氧呼吸的好去处!石景山区炮山城市森林公园坐落在石景山五里坨地区的炮山城市森林公园置身其中仿佛忘了周边钢筋水泥……想控糖的年轻人你真的了解代糖吗?时下,年轻人的一些不健康的生活习惯,例如熬夜刷剧、常喝奶茶、喜食油腻、久坐不动、不爱运动等,让糖尿病、高血压、癌症等既往老年群体高发的疾病正在不知不觉地盯上他们。为了不让……新赛季谁将是东部最强三巨头?看完西部的三人组真是默默为东部捏一把汗,相比西部的星光璀璨,东部就有点星光暗淡了,不过有几支球队经过休赛季的引援,也表现出极强的竞争力,今天我们就一起来盘点一下凯尔特人塔图姆布……实力即正义!7000万像素分辨率造就的人文镜头岩石星40mm岩石星40mmF5。6,一个擅长于人文故事的全能型镜头。它的颜值精致且典雅,有着高达7000万的像素分辨率,近乎于完美的畸变控制,它是简朴便携的摄影装备的代表之一。……定了,又有三款限定皮肤即将返场,最后一次返场,玄武志不要错过大家好,我是王者老叔,因为夏日盛典主题活动,最近王者峡谷不可谓不热闹,夏日之旅第一站不仅上线了马可波罗,后裔等多款高级皮肤,还有免费送皮肤的活动。现在夏日之旅到达了第二站,官方……最早的游山玩水,您可要悠着点儿千里万里游山玩水之感,我还一字未写,没有这第一次的破冰,写到哪里总感觉是无源之水。在报纸杂志上从未出现过旅游二字的年代,用亲情,友情加大二八车,组织了第一次两日游山。……美媒詹姆斯差1326分问鼎NBA历史得分王下赛季多少场能达成直播吧7月26日讯目前詹姆斯生涯常规赛总得分为37062分,而NBA历史得分王贾巴尔生涯总得分为38387分,詹姆斯距离成为NBA历史得分王还差1326分。美媒发问:……
友情链接:快好找快生活快百科快传网中准网文好找聚热点快软网