内存结构
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。