GoLang底层|GoLang之堆内存系列一(堆内存管理)


文章目录

  • GoLang之堆内存系列一(堆内存管理)
    • 1.堆内存结构
    • 2.heapArena
      • 2.1heapArena
      • 2.2heapArena.bitmap
      • 2.3heapArena.pageInUse
      • 2.4heapArena.pageMarks
      • 2.5heapArena.spans
    • 3.mspan
      • 3.1mspan
      • 3.2mspan.nelems
      • 3.3mspan.freeIndex
      • 3.4mspan.allocBits
      • 3.3mspan.gcmarkBits
    • 4.spanClass
    • 5.mheap
    • 6.mcentral
    • 7.mcache

GoLang之堆内存系列一(堆内存管理) 注:这一次,我们基于Go1.16,介绍了堆内存的基本结构,以及负责堆内存管理的主要数据结构。
关键是理解mcentral和mcache的合作方式,以及heapArena与mspan中这些重要的位图标记。结合内存分配与GC,理解它们是如何发挥作用的。
1.堆内存结构
Go语言的runtime将堆地址空间划分成了一个一个的arena,arena区域的起始地址被定义为常量arenaBaseOffset。
在amd64架构的Linux环境下,每个arena的大小是64MB,起始地址也对齐到64MB,每个arena包含8192个page,所以每个page大小为8KB。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

因为程序运行起来所需要分配的内存块有大有小,而分散的、大小不一的碎片化内存一方面可能降低内存使用率:
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

另一方面要找到大小合适的内存块的代价会因碎片化而增加。
为降低碎片化内存给程序性能造成的不良影响,Go语言的堆分配采用了与tcmalloc内存分配器类似的算法。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

简单来讲就是:按照一组预置的大小规格把内存页划分成块,然后把不同规格的内存块放入对应的空闲链表中。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

程序申请内存时,分配器会先根据要申请的内存大小找到最匹配的规格,然后从对应空闲链表中分配一个内存块。
Go 1.16 runtime包给出了67种预置的大小规格,最小8字节,最大32KB。
所以在划分的整整齐齐的arena里,又会按需划分出不同的span,每个span包含一组连续的page,并且按照特定规格划分成了等大的内存块(在span里有很多等大的内存块)。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

arena, span, page和内存块组成了堆内存,而在堆内存之外,有一大票用于管理堆内存的数据结构。
例如,mheap用于管理整个堆内存;一个arena对应一个heapArena结构; 一个span对应一个mspan结构。
通过它们可以知道某个内存块是否已分配;已分配的内存用作指针还是标量;是否已被GC标记;是否等待清扫等信息。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

2.heapArena 2.1heapArena
heapArena是在Go的堆之外分配和管理的,其结构定义代码如下:
type heapArena struct { bitmap[heapArenaBitmapBytes]byte//heapArenaBitmapBytes是一个常量,数值是多少不确定。比较难算 spans[pagesPerArena]*mspan pageInUse[pagesPerArena / 8]uint8 pageMarks[pagesPerArena / 8]uint8 pageSpecials [pagesPerArena / 8]uint8 checkmarks*checkmarksMap zeroedBaseuintptr }

heapArena这里存储着arena的元数据,里面有一群位图标记。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

2.2heapArena.bitmap
bitmap位图:
(1)用一位标记这个arena中,一个指针大小的内存单元到底是指针还是标量;
(2)再用一位来标记这块内存空间的后续单元是否包含指针。
而且为了便于操作,bitmap中用一字节标记arena中4个指针大小的内存空间:低4位用于标记指针/标量;高4位用于标记扫描/终止。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

例如在arena起始处分配一个slice,slice结构包括一个元素指针,一个长度,以及一个容量。对应的bitmap标记位图中:
(1)第一字节的第0位到第2位标记这三个字段是指针还是标量;
(2)第4位到第6位标记三个字段是否需要继续扫描。
【GoLang底层|GoLang之堆内存系列一(堆内存管理)】GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

2.3heapArena.pageInUse
pageInUse是个uint8类型的数组,长度为1024,所以一共8192位。结合这个名字,看起来似乎是标记哪些页面被使用了。但实际上,这个位图只标记处于使用状态(mSpanInUse)的span的第一个page。
例如arena中连续三个span分别包含1,2,3个page,pageInUse位图标记情况如下图所示:
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

2.4heapArena.pageMarks
pageMarks这个位图看起来应该和GC标记有点儿关系,它的用法和pageInUse一样,只标记每个span的第一个page。在GC标记阶段会修改这个位图,标记哪些span中存在被标记的对象; 在GC清扫阶段会根据这个位图,来释放不含标记对象的span。
2.5heapArena.spans
spans是个*mspan类型的数组,大小为8192,正好对应arena中8192个page,所以用于定位一个page对应的mspan在哪儿。
3.mspan 3.1mspan
mspan管理着span中一组连续的page,mspan同mcentrla一样,将划分的内存块规格类型记录在spanClass中。
type mspan struct { next*mspan prev*mspan list*mSpanList startAddruintptr npagesuintptr manualFreeList gclinkptr freeindexuintptr nelemsuintptr allocCacheuint64 allocBits*gcBits gcmarkBits*gcBits sweepgenuint32 divMuluint16 baseMaskuint16 allocCountuint16 spanclassspanClass statemSpanStateBox needzerouint8 divShiftuint8 divShift2uint8 elemsizeuintptr limituintptr speciallockmutex specials*special }

// mSpanList heads a linked list of spans. // //go:notinheap type mSpanList struct { first *mspan // first span in list, or nil if none last*mspan // last span in list, or nil if none }

3.2mspan.nelems
nelems记录着当前span共划分成了多少个内存块。
3.3mspan.freeIndex
freeIndex记录着下个空闲内存块的索引。
3.4mspan.allocBits
与heapArena不同,mspan这里的位图标记,面向的是划分好的内存块单元,allocBits位图用于标记哪些内存块已经被分配了。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

3.3mspan.gcmarkBits
gcmarkBits是当前span的标记位图,在GC标记阶段会对这个位图进行标记,一个二进制位对应span中的一个内存块。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

到GC清扫阶段会释放掉旧的allocBits,然后把标记好的gcmarkBits用作allocBits,这样未被GC标记的内存块就能回收利用了。当然会重新分配一段清零的内存给gcmarkBits位图。
4.spanClass
spanClass是这样用的:
高七位标记内存块大小规格编号,runtime提供的预置规格对应编号1到67,编号0留出来,编号0对应大于32KB的大块内存,一共68种([8B,32KB])。
然后每种规格会按照是否不需要GC扫描进一步区分开来,用最低位来标识:
(1)包含指针的需要GC扫描,归为scannable这一类;
(2)不含指针的归为noscan这一类。
68*2=136,所以一共分成136种。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

5.mheap
mhep中有一个全局的mspan管理中心,即mheap.central, 它是一个长度为136的数组,数组元素是一个mcetral数据结构加上一个padding。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

6.mcentral
mcentral怎么管理mspan呢?
type mcentral struct { spanclass spanClass partial[2]spanSet full[2]spanSet }

实际上,一个mcentral对应一种mspan规格类型,同样记录在spanclass中,一共有136种。
每种spanclass的mcentral中,会进一步将已用尽(full)与未用尽(partial)的mspan分别管理。每一种又会放到两个并发安全的set中:一个是已清扫的;另一个是未清扫的。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

全局mspan管理中心方便取用各种规格类型的mspan。但是为保障多个P之间并发安全,免不了频繁加锁、解锁。
为降低多个P之间的竞争性,Go语言的每个P都有一个本地小对象缓存,也就是mcache,从这里取用就不用再加锁了。
7.mcache
type mcache struct { nextSample uintptr scanAllocuintptr tinyuintptr tinyoffset uintptr tinyAllocs uintptr alloc[numSpanClasses]*mspan //numSpanClasses = _NumSizeClasses << 1 //_NumSizeClasses = 68 stackcache [_NumStackOrders]stackfreelist flushGenuint32 }

mcache这里有一个长度为136的*mspan类型的数组,还有专门用于分配小于16字节的noscan类型的tiny内存。
当前P需要用到特定规格类型的mspan时,先去本地缓存这里找对应的mspan;如果没有或者用完了,就去mcentral这里获取一个放到本地,把已用尽的归还到对应mcentral的full set中。
GoLang底层|GoLang之堆内存系列一(堆内存管理)
文章图片

注:arena:”额润呢“
Tcmalloc:T C 模 额唠嗑
span :死判
mheap : m 黑p
mcentral:m森戳
padding:拍丁
scannable: 死砍呢宝
bitmap:比特map

    推荐阅读