内存结构

golang内存划分在arena里,又会按需划分出不同的span,每个span包含一组连续的page,并且按照特定规格划分成了等大的内存块。

Go 1.16 runtime包给出了67种预置的大小规格,最小8字节,最大32KB。按需把对象放到指定的span中,防止产生过多的内存碎片

arena, span, page和内存块组成了堆内存,而在堆内存之外,有一票用于管理堆内存的数据结构。一個全局mheap,一个arena对应一个heapArena结构,一个span对应一个mspan结构。通过它们可以知道某个内存块是否已分配;已分配的内存用作指针还是标量;是否已被GC标记;是否等待清扫等信息。

mheap

结构如下,

每种mental对应一直mspan 高七位标记内存块大小规格编号,runtime提供的预置规格对应编号1到67,

编号0留出来,对应大于32KB的大块内存,一共68种。

然后每种规格会按照是否不需要GC扫描进一步区分开来,用最低位来标识:

(1)包含指针的需要GC扫描,归为scannable这一类;

(2)不含指针的归为noscan这一类。

full 用来管理span有无清扫

为降低多个P之间的竞争性,Go语言的每个P都有一个本地小对象缓存,也就是mcache,从这里取用就不用再加锁了

mcache

type mcache struct {
  nextSample uintptr
  scanAlloc  uintptr
  tiny       uintptr
  tinyoffset uintptr
  tinyAllocs uintptr
  alloc      [numSpanClasses]*mspan
  stackcache [_NumStackOrders]stackfreelist
  flushGen   uint32
}

mcache这里有一个长度为136的、*mspan类型的数组,还有专门用于分配小于16字节的noscan类型的tiny内存。

当前P需要用到特定规格类型的mspan时,先去本地缓存这里找对应的mspan;如果没有或者用完了,就去mcentral这里获取一个放到本地,把已用尽的归还到对应mcentral的full set中。

heapArena

bitmap位图:

(1)用一位标记这个arena中,一个指针大小的内存单元到底是指针还是标量;

(2)再用一位来标记这块内存空间的后续单元是否包含指针。

而且为了便于操作,bitmap中用一字节标记arena中4个指针大小的内存空间:低4位用于标记指针/标量;高4位用于标记扫描/终止。例如在arena起始处分配一个slice,slice结构包括一个元素指针一个长度, 以及一个容量。 对应的bitmap标记位图中:

(1)第一字节的第0位到第2位标记这三个字段是指针还是标量;

(2)第4位到第6位标记三个字段是否需要继续扫描。

pageInUse

是个uint8类型的数组,长度为1024,所以一共8192位。结合这个名字,看起来似乎是标记哪些页面被使用了。但实际上,这个位图只标记处于使用状态(mSpanInUse)的span的第一个page。(开始才会标为1,后续的span不会标记)

heapArena.pageMarks

pageMarks

它的用法和pageInUse一样,只标记每个span的第一个page。在GC标记阶段会修改这个位图,标记哪些span中存在被标记的对象;在GC清扫阶段会根据这个位图,来释放不含标记对象的span。

heapArena.spans

spans是个*mspan类型的数组,大小为8192,正好对应arena中8192个page,所以用于定位一个page对应的mspan在哪儿。

mspan管理着span中一组连续的page,划分的内存块规格类型记录在spanclass中。

spanclass是这样用的:

高七位标记内存块大小规格编号,runtime提供的预置规格对应编号1到67,编号0留出来,对应大于32KB的大块内存,一共68种。然后每种规格会按照是否不需要GC扫描进一步区分开来,

用最低位来标识:

(1)包含指针的需要GC扫描,归为scannable这一类;

(2)不含指针的归为noscan这一类。

nelems

记录着当前span共划分成了多少个内存块。

freeIndex

记录着下个空闲内存块的索引。与heapArena不同,mspan这里的位图标记,面向的是划分好的内存块单元,

allocBits

位图用于标记哪些内存块已经被分配了。

gcmarkBits

到GC清扫阶段会释放掉旧的allocBits,然后把标记好的gcmarkBits用作allocBits,这样未被GC标记的内存块就能回收利用了。当然会重新分配一段清零的内存给gcmarkBits位图。

接下来技术内存分配过程

辅助GC

如果程序申请堆内存时正处于GC标记阶段,内存申请的速度超过了GC标记的速度, 那么新来的协程药物辅助GC申请一字节内存空间需要做多少扫描工作,最少要扫描64KB。

1、当前G信用:如果我要用4kb,但是扫描了64kb,不公平 协程每次执行辅助GC,多出来的部分会作为信用存储到当前G中

2、全局信用:窃取信用后台的GC mark worker执行扫描任务,会在全局gcController的bgScanCredit这里积累信用。如果能够窃取足够多的信用值来抵消当前协程背负的债务,那也就不用执行辅助GC了~

空间分配

if size <= maxSmallSize {
  if noscan && size < maxTinySize {
    // 使用tiny allocator分配
  } else {
    // 使用mcache.alloc中对应的mspan分配
  }
} else {
  // 直接根据需要的页面数,分配大的mspan
}

而对于小于16字节的内存分配,也不直接匹配预置内存规格 而tiny allocator能够将几个小块的内存分配请求合并,所以16次1字节的内存分配请求可以合并到一个16字节的内存块中:

每个P的mcache这里有专门用于tiny allocator的内存(mcache.tiny),这是一个16字节大小的内存单元,mcache.tinyoffset记录这段内存已经用到哪里了:

1、如果tiny allocator要分配size大小的内存空间,而mcache中的tinyoffset经对齐后还够分配size大小的内存,就在tiny内存块中直接分配。

2、如果剩余的空间不够了,就从当前P的mcache中找到对应的mspan,重新拿一个16字节大小的内存块过来用;如果本地缓存中相应规格的mspan也没有空间了,就会从mcentral中拿一个新的mspan过来。分配完以后,如果新拿来的内存块的剩余空间比旧内存块的剩余空间还大,那就用新的内存块把旧的tiny替换掉。

位图标记

上述有,技术标记哪些用了

收尾工作

(1)判断如果处在GC的标记阶段就标记新分配的对象;

(2)在memory profile开启的情况下,每分配nextSample字节内存以后,就进行一次采样;

(3)在分配的过程中,size可能是向上对齐过的,所以可能会变大。而dataSize保存了原来真实的size值,所以要从分配内存的goroutine的gcAssistBytes中减去因size对齐而额外多分配的大小;

(4)最后检测如果达到了GC的触发条件,就发起GC。