Golang从1.5开始引入了三色GC, 通过屡次改进, 当前的1.9版本的GC停顿时间已经能够作到极短.
停顿时间的减小意味着"最大响应时间"的缩短, 这也让go更适合编写网络服务程序.
这篇文章将经过分析golang的源代码来说解go中的三色GC的实现原理.html
这个系列分析的golang源代码是Google官方的实现的1.9.2版本, 不适用于其余版本和gccgo等其余实现,
运行环境是Ubuntu 16.04 LTS 64bit.
首先会讲解基础概念, 而后讲解分配器, 再讲解收集器的实现.node
go在程序启动时会分配一块虚拟内存地址是连续的内存, 结构以下:linux
这一块内存分为了3个区域, 在X64上大小分别是512M, 16G和512G, 它们的做用以下:c++
arenagit
arena区域就是咱们一般说的heap, go从heap分配的内存都在这个区域中.github
bitmapgolang
bitmap区域用于表示arena区域中哪些地址保存了对象, 而且对象中哪些地址包含了指针.
bitmap区域中一个byte(8 bit)对应了arena区域中的四个指针大小的内存, 也就是2 bit对应一个指针大小的内存.
因此bitmap区域的大小是 512GB / 指针大小(8 byte) / 4 = 16GB.web
bitmap区域中的一个byte对应arena区域的四个指针大小的内存的结构以下,
每个指针大小的内存都会有两个bit分别表示是否应该继续扫描和是否包含指针:算法
bitmap中的byte和arena的对应关系从末尾开始, 也就是随着内存分配会向两边扩展:json
spans
spans区域用于表示arena区中的某一页(Page)属于哪一个span, 什么是span将在下面介绍.
spans区域中一个指针(8 byte)对应了arena区域中的一页(在go中一页=8KB).
因此spans的大小是 512GB / 页大小(8KB) * 指针大小(8 byte) = 512MB.
spans区域的一个指针对应arena区域的一页的结构以下, 和bitmap不同的是对应关系会从开头开始:
不少讲解go的文章和书籍中都提到过, go会自动肯定哪些对象应该放在栈上, 哪些对象应该放在堆上.
简单的来讲, 当一个对象的内容可能在生成该对象的函数结束后被访问, 那么这个对象就会分配在堆上.
在堆上分配对象的状况包括:
在C语言中函数返回在栈上的对象的指针是很是危险的事情, 但在go中倒是安全的, 由于这个对象会自动在堆上分配.
go决定是否使用堆分配对象的过程也叫"逃逸分析".
GC在标记时须要知道哪些地方包含了指针, 例如上面提到的bitmap区域涵盖了arena区域中的指针信息.
除此以外, GC还须要知道栈空间上哪些地方包含了指针,
由于栈空间不属于arena区域, 栈空间的指针信息将会在函数信息里面.
另外, GC在分配对象时也须要根据对象的类型设置bitmap区域, 来源的指针信息将会在类型信息里面.
总结起来go中有如下的GC Bitmap:
span是用于分配对象的区块, 下图是简单说明了Span的内部结构:
一般一个span包含了多个大小相同的元素, 一个元素会保存一个对象, 除非:
span中有一个freeindex标记下一次分配对象时应该开始搜索的地址, 分配后freeindex会增长,
在freeindex以前的元素都是已分配的, 在freeindex以后的元素有可能已分配, 也有可能未分配.
span每次GC之后均可能会回收掉一些元素, allocBits用于标记哪些元素是已分配的, 哪些元素是未分配的.
使用freeindex + allocBits能够在分配时跳过已分配的元素, 把对象设置在未分配的元素中,
但由于每次都去访问allocBits效率会比较慢, span中有一个整数型的allocCache用于缓存freeindex开始的bitmap, 缓存的bit值与原值相反.
gcmarkBits用于在gc时标记哪些对象存活, 每次gc之后gcmarkBits会变为allocBits.
须要注意的是span结构自己的内存是从系统分配的, 上面提到的spans区域和bitmap区域都只是一个索引.
span根据大小能够分为67个类型, 以下:
// class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% // 2 16 8192 512 0 43.75% // 3 32 8192 256 0 46.88% // 4 48 8192 170 32 31.52% // 5 64 8192 128 0 23.44% // 6 80 8192 102 32 19.07% // 7 96 8192 85 32 15.95% // 8 112 8192 73 16 13.56% // 9 128 8192 64 0 11.72% // 10 144 8192 56 128 11.82% // 11 160 8192 51 32 9.73% // 12 176 8192 46 96 9.59% // 13 192 8192 42 128 9.25% // 14 208 8192 39 80 8.12% // 15 224 8192 36 128 8.15% // 16 240 8192 34 32 6.62% // 17 256 8192 32 0 5.86% // 18 288 8192 28 128 12.16% // 19 320 8192 25 192 11.80% // 20 352 8192 23 96 9.88% // 21 384 8192 21 128 9.51% // 22 416 8192 19 288 10.71% // 23 448 8192 18 128 8.37% // 24 480 8192 17 32 6.82% // 25 512 8192 16 0 6.05% // 26 576 8192 14 128 12.33% // 27 640 8192 12 512 15.48% // 28 704 8192 11 448 13.93% // 29 768 8192 10 512 13.94% // 30 896 8192 9 128 15.52% // 31 1024 8192 8 0 12.40% // 32 1152 8192 7 128 12.41% // 33 1280 8192 6 512 15.55% // 34 1408 16384 11 896 14.00% // 35 1536 8192 5 512 14.00% // 36 1792 16384 9 256 15.57% // 37 2048 8192 4 0 12.45% // 38 2304 16384 7 256 12.46% // 39 2688 8192 3 128 15.59% // 40 3072 24576 8 0 12.47% // 41 3200 16384 5 384 6.22% // 42 3456 24576 7 384 8.83% // 43 4096 8192 2 0 15.60% // 44 4864 24576 5 256 16.65% // 45 5376 16384 3 256 10.92% // 46 6144 24576 4 0 12.48% // 47 6528 32768 5 128 6.23% // 48 6784 40960 6 256 4.36% // 49 6912 49152 7 768 3.37% // 50 8192 8192 1 0 15.61% // 51 9472 57344 6 512 14.28% // 52 9728 49152 5 512 3.64% // 53 10240 40960 4 0 4.99% // 54 10880 32768 3 128 6.24% // 55 12288 24576 2 0 11.45% // 56 13568 40960 3 256 9.99% // 57 14336 57344 4 0 5.35% // 58 16384 16384 1 0 12.49% // 59 18432 73728 4 0 11.11% // 60 19072 57344 3 128 3.57% // 61 20480 40960 2 0 6.87% // 62 21760 65536 3 256 6.25% // 63 24576 24576 1 0 11.45% // 64 27264 81920 3 128 10.00% // 65 28672 57344 2 0 4.91% // 66 32768 32768 1 0 12.50%
以类型(class)为1的span为例,
span中的元素大小是8 byte, span自己占1页也就是8K, 一共能够保存1024个对象.
在分配对象时, 会根据对象的大小决定使用什么类型的span,
例如16 byte的对象会使用span 2, 17 byte的对象会使用span 3, 32 byte的对象会使用span 3.
从这个例子也能够看到, 分配17和32 byte的对象都会使用span 3, 也就是说部分大小的对象在分配时会浪费必定的空间.
有人可能会注意到, 上面最大的span的元素大小是32K, 那么分配超过32K的对象会在哪里分配呢?
超过32K的对象称为"大对象", 分配大对象时, 会直接从heap分配一个特殊的span,
这个特殊的span的类型(class)是0, 只包含了一个大对象, span的大小由对象的大小决定.
特殊的span加上的66个标准的span, 一共组成了67个span类型.
在前一篇中我提到了P是一个虚拟的资源, 同一时间只能有一个线程访问同一个P, 因此P中的数据不须要锁.
为了分配对象时有更好的性能, 各个P中都有span的缓存(也叫mcache), 缓存的结构以下:
各个P中按span类型的不一样, 有67*2=134个span的缓存,
其中scan和noscan的区别在于,
若是对象包含了指针, 分配对象时会使用scan的span,
若是对象不包含指针, 分配对象时会使用noscan的span.
把span分为scan和noscan的意义在于,
GC扫描对象的时候对于noscan的span能够不去查看bitmap区域来标记子对象, 这样能够大幅提高标记的效率.
在分配对象时将会从如下的位置获取适合的span用于分配:
在P中缓存span的作法跟CoreCLR中线程缓存分配上下文(Allocation Context)的作法类似,
均可以让分配对象时大部分时候不须要线程锁, 改进分配的性能.
go从堆分配对象时会调用newobject函数, 这个函数的流程大体以下:
首先会检查GC是否在工做中, 若是GC在工做中而且当前的G分配了必定大小的内存则须要协助GC作必定的工做,
这个机制叫GC Assist, 用于防止分配内存太快致使GC回收跟不上的状况发生.
以后会判断是小对象仍是大对象, 若是是大对象则直接调用largeAlloc从堆中分配,
若是是小对象分3个阶段获取可用的span, 而后从span中分配对象:
这三个阶段的详细结构以下图:
分配对象涉及的数据类型包含:
p: 前一篇提到过, P是协程中的用于运行go代码的虚拟资源
m: 前一篇提到过, M目前表明系统线程
g: 前一篇提到过, G就是goroutine
mspan: 用于分配对象的区块
mcentral: 全局的mspan缓存, 一共有67*2=134个
mheap: 用于管理heap的对象, 全局只有一个
go从堆分配对象时会调用newobject函数, 先从这个函数看起:
// implementation of new builtin // compiler (both frontend and SSA backend) knows the signature // of this function func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) }
// Allocate an object of size bytes. // Small objects are allocated from the per-P cache's free lists. // Large objects (> 32 kB) are allocated straight from the heap. func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { if gcphase == _GCmarktermination { throw("mallocgc called with gcphase == _GCmarktermination") } if size == 0 { return unsafe.Pointer(&zerobase) } if debug.sbrk != 0 { align := uintptr(16) if typ != nil { align = uintptr(typ.align) } return persistentalloc(size, align, &memstats.other_sys) } // 判断是否要辅助GC工做 // gcBlackenEnabled在GC的标记阶段会开启 // assistG is the G to charge for this allocation, or nil if // GC is not currently active. var assistG *g if gcBlackenEnabled != 0 { // Charge the current user G for this allocation. assistG = getg() if assistG.m.curg != nil { assistG = assistG.m.curg } // Charge the allocation against the G. We'll account // for internal fragmentation at the end of mallocgc. assistG.gcAssistBytes -= int64(size) // 会按分配的大小判断须要协助GC完成多少工做 // 具体的算法将在下面讲解收集器时说明 if assistG.gcAssistBytes < 0 { // This G is in debt. Assist the GC to correct // this before allocating. This must happen // before disabling preemption. gcAssistAlloc(assistG) } } // 增长当前G对应的M的lock计数, 防止这个G被抢占 // Set mp.mallocing to keep from being preempted by GC. mp := acquirem() if mp.mallocing != 0 { throw("malloc deadlock") } if mp.gsignal == getg() { throw("malloc during signal") } mp.mallocing = 1 shouldhelpgc := false dataSize := size // 获取当前G对应的M对应的P的本地span缓存(mcache) // 由于M在拥有P后会把P的mcache设到M中, 这里返回的是getg().m.mcache c := gomcache() var x unsafe.Pointer noscan := typ == nil || typ.kind&kindNoPointers != 0 // 判断是否小对象, maxSmallSize当前的值是32K if size <= maxSmallSize { // 若是对象不包含指针, 而且对象的大小小于16 bytes, 能够作特殊处理 // 这里是针对很是小的对象的优化, 由于span的元素最小只能是8 byte, 若是对象更小那么不少空间都会被浪费掉 // 很是小的对象能够整合在"class 2 noscan"的元素(大小为16 byte)中 if noscan && size < maxTinySize { // Tiny allocator. // // Tiny allocator combines several tiny allocation requests // into a single memory block. The resulting memory block // is freed when all subobjects are unreachable. The subobjects // must be noscan (don't have pointers), this ensures that // the amount of potentially wasted memory is bounded. // // Size of the memory block used for combining (maxTinySize) is tunable. // Current setting is 16 bytes, which relates to 2x worst case memory // wastage (when all but one subobjects are unreachable). // 8 bytes would result in no wastage at all, but provides less // opportunities for combining. // 32 bytes provides more opportunities for combining, // but can lead to 4x worst case wastage. // The best case winning is 8x regardless of block size. // // Objects obtained from tiny allocator must not be freed explicitly. // So when an object will be freed explicitly, we ensure that // its size >= maxTinySize. // // SetFinalizer has a special case for objects potentially coming // from tiny allocator, it such case it allows to set finalizers // for an inner byte of a memory block. // // The main targets of tiny allocator are small strings and // standalone escaping variables. On a json benchmark // the allocator reduces number of allocations by ~12% and // reduces heap size by ~20%. off := c.tinyoffset // Align tiny pointer for required (conservative) alignment. if size&7 == 0 { off = round(off, 8) } else if size&3 == 0 { off = round(off, 4) } else if size&1 == 0 { off = round(off, 2) } if off+size <= maxTinySize && c.tiny != 0 { // The object fits into existing tiny block. x = unsafe.Pointer(c.tiny + off) c.tinyoffset = off + size c.local_tinyallocs++ mp.mallocing = 0 releasem(mp) return x } // Allocate a new maxTinySize block. span := c.alloc[tinySpanClass] v := nextFreeFast(span) if v == 0 { v, _, shouldhelpgc = c.nextFree(tinySpanClass) } x = unsafe.Pointer(v) (*[2]uint64)(x)[0] = 0 (*[2]uint64)(x)[1] = 0 // See if we need to replace the existing tiny block with the new one // based on amount of remaining free space. if size < c.tinyoffset || c.tiny == 0 { c.tiny = uintptr(x) c.tinyoffset = size } size = maxTinySize } else { // 不然按普通的小对象分配 // 首先获取对象的大小应该使用哪一个span类型 var sizeclass uint8 if size <= smallSizeMax-8 { sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv] } else { sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv] } size = uintptr(class_to_size[sizeclass]) // 等于sizeclass * 2 + (noscan ? 1 : 0) spc := makeSpanClass(sizeclass, noscan) span := c.alloc[spc] // 尝试快速的从这个span中分配 v := nextFreeFast(span) if v == 0 {