1. Golang GC 发展算法
Golang 从第一个版本以来,GC 一直是你们诟病最多的。可是每个版本的发布基本都伴随着 GC 的改进。下面列出一些比较重要的改动。数据结构
2. GC 算法简介
这一小节介绍三种经典的 GC 算法:并发
3. 引用计数app
引用计数的思想很是简单:每一个单元维护一个域,保存其它单元指向它的引用数量(相似有向图的入度)。当引用数量为 0 时,将其回收。引用计数是渐进式的,可以将内存管理的开销分布到整个程序之中。C++ 的 share_ptr 使用的就是引用计算方法。ide
引用计数算法实现通常是把全部的单元放在一个单元池里,好比相似 free list。这样全部的单元就被串起来了,就能够进行引用计数了。新分配的单元计数值被设置为 1(注意不是 0,由于申请通常都说 ptr = new object 这种)。每次有一个指针被设为指向该单元时,该单元的计数值加 1;而每次删除某个指向它的指针时,它的计数值减 1。当其引用计数为 0 的时候,该单元会被进行回收。虽然这里说的比较简单,实现的时候仍是有不少细节须要考虑,好比删除某个单元的时候,那么它指向的全部单元都须要对引用计数减 1。那么若是这个时候,发现其中某个指向的单元的引用计数又为 0,那么是递归的进行仍是采用其余的策略呢?递归处理的话会致使系统颠簸。关于这些细节这里就不讨论了,能够参考文章后面的给的参考资料。函数
优势oop
缺点性能
4. 标记-清扫
标记-清扫算法是第一种自动内存管理,基于追踪的垃圾收集算法。算法思想在 70 年代就提出了,是一种很是古老的算法。内存单元并不会在变成垃圾马上回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是 STW,转而执行垃圾回收程序。垃圾回收程序对全部的存活单元进行一次全局遍历肯定哪些单元能够回收。算法分两个部分:标记(mark)和清扫(sweep)。标记阶段代表全部的存活单元,清扫阶段将垃圾单元回收。可视化能够参考下图。fetch
标记-清扫算法的优势也就是基于追踪的垃圾回收算法具备的优势:避免了引用计数算法的缺点(不能处理循环引用,须要维护指针)。缺点也很明显,须要 STW。ui
三色标记算法
三色标记算法是对标记阶段的改进,原理以下:
可视化以下。
三色标记的一个明显好处是可以让用户程序和 mark 并发的进行,具体能够参考论文:《On-the-fly garbage collection: an exercise in cooperation.》。Golang 的 GC 实现也是基于这篇论文,后面再具体说明。
5. 节点复制
节点复制也是基于追踪的算法。其将整个堆等分为两个半区(semi-space),一个包含现有数据,另外一个包含已被废弃的数据。节点复制式垃圾收集从切换(flip)两个半区的角色开始,而后收集器在老的半区,也就是 Fromspace 中遍历存活的数据结构,在第一次访问某个单元时把它复制到新半区,也就是 Tospace 中去。在 Fromspace 中全部存活单元都被访问过以后,收集器在 Tospace 中创建一个存活数据结构的副本,用户程序能够从新开始运行了。
优势
缺点
6. 分代收集
基于追踪的垃圾回收算法(标记-清扫、节点复制)一个主要问题是在生命周期较长的对象上浪费时间(长生命周期的对象是不须要频繁扫描的)。同时,内存分配存在这么一个事实 “most object die young”。基于这两点,分代垃圾回收算法将对象按生命周期长短存放到堆上的两个(或者更多)区域,这些区域就是分代(generation)。对于新生代的区域的垃圾回收频率要明显高于老年代区域。
分配对象的时候重新生代里面分配,若是后面发现对象的生命周期较长,则将其移到老年代,这个过程叫作 promote。随着不断 promote,最后新生代的大小在整个堆的占用比例不会特别大。收集的时候集中主要精力在新生代就会相对来讲效率更高,STW 时间也会更短。
优势
缺点
7. Golang GC
7.1 Overview
在说 Golang 的具体垃圾回收流程时,咱们先来看一下几个基本的问题。
1. 什么时候触发 GC
在堆上分配大于 32K byte 对象的时候进行检测此时是否知足垃圾回收条件,若是知足则进行垃圾回收。
1 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { 2 ... 3 shouldhelpgc := false 4 // 分配的对象小于 32K byte 5 if size <= maxSmallSize { 6 ... 7 } else { 8 shouldhelpgc = true 9 ... 10 } 11 ... 12 // gcShouldStart() 函数进行触发条件检测 13 if shouldhelpgc && gcShouldStart(false) { 14 // gcStart() 函数进行垃圾回收 15 gcStart(gcBackgroundMode, false) 16 } 17 }
上面是自动垃圾回收,还有一种是主动垃圾回收,经过调用 runtime.GC(),这是阻塞式的。
1 // GC runs a garbage collection and blocks the caller until the 2 // garbage collection is complete. It may also block the entire 3 // program. 4 func GC() { 5 gcStart(gcForceBlockMode, false) 6 }
2. GC 触发条件
触发条件主要关注下面代码中的中间部分:forceTrigger || memstats.heap_live >= memstats.gc_trigger 。forceTrigger 是 forceGC 的标志;后面半句的意思是当前堆上的活跃对象大于咱们初始化时候设置的 GC 触发阈值。在 malloc 以及 free 的时候 heap_live 会一直进行更新,这里就再也不展开了。
1 // gcShouldStart returns true if the exit condition for the _GCoff 2 // phase has been met. The exit condition should be tested when 3 // allocating. 4 // 5 // If forceTrigger is true, it ignores the current heap size, but 6 // checks all other conditions. In general this should be false. 7 func gcShouldStart(forceTrigger bool) bool { 8 return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0 9 } 10 11 //初始化的时候设置 GC 的触发阈值 12 func gcinit() { 13 _ = setGCPercent(readgogc()) 14 memstats.gc_trigger = heapminimum 15 ... 16 } 17 // 启动的时候经过 GOGC 传递百分比 x 18 // 触发阈值等于 x * defaultHeapMinimum (defaultHeapMinimum 默认是 4M) 19 func readgogc() int32 { 20 p := gogetenv("GOGC") 21 if p == "off" { 22 return -1 23 } 24 if n, ok := atoi32(p); ok { 25 return n 26 } 27 return 100 28 }
3. 垃圾回收的主要流程
三色标记法,主要流程以下:
详细的过程以下图所示,具体可参考 [9]。
关于上图有几点须要说明的是。
另外针对上图各个阶段对应 GCPhase 以下:
7.2 写屏障 (write barrier)
关于 write barrier,彻底能够另外写成一篇文章,因此这里只简单介绍一下,这篇文章的重点仍是 Golang 的 GC。垃圾回收中的 write barrier 能够理解为编译器在写操做时特地插入的一段代码,对应的还有 read barrier。
为何须要 write barrier,很简单,对于和用户程序并发运行的垃圾回收算法,用户程序会一直修改内存,因此须要记录下来。
Golang 1.7 以前的 write barrier 使用的经典的 Dijkstra-style insertion write barrier [Dijkstra ‘78], STW 的主要耗时就在 stack re-scan 的过程。自 1.8 以后采用一种混合的 write barrier 方式 (Yuasa-style deletion write barrier [Yuasa ‘90] 和 Dijkstra-style insertion write barrier [Dijkstra ‘78])来避免 re-scan。具体的能够参考 17503-eliminate-rescan。
7.3 标记
下面的源码仍是基于 go1.8rc3。这个版本的 GC 代码相比以前改动仍是挺大的,咱们下面尽可能只关注主流程。垃圾回收的代码主要集中在函数 gcStart() 中。
1 // gcStart 是 GC 的入口函数,根据 gcMode 作处理。 2 // 1. gcMode == gcBackgroundMode(后台运行,也就是并行), _GCoff -> _GCmark 3 // 2. 不然 GCoff -> _GCmarktermination,这个时候就是主动 GC 4 func gcStart(mode gcMode, forceTrigger bool) { 5 ... 6 }
1. STW phase 1
在 GC 开始以前的准备工做。
1 func gcStart(mode gcMode, forceTrigger bool) { 2 ... 3 //在后台启动 mark worker 4 if mode == gcBackgroundMode { 5 gcBgMarkStartWorkers() 6 } 7 ... 8 // Stop The World 9 systemstack(stopTheWorldWithSema) 10 ... 11 if mode == gcBackgroundMode { 12 // GC 开始前的准备工做 13 14 //处理设置 GCPhase,setGCPhase 还会 enable write barrier 15 setGCPhase(_GCmark) 16 17 gcBgMarkPrepare() // Must happen before assist enable. 18 gcMarkRootPrepare() 19 20 // Mark all active tinyalloc blocks. Since we're 21 // allocating from these, they need to be black like 22 // other allocations. The alternative is to blacken 23 // the tiny block on every allocation from it, which 24 // would slow down the tiny allocator. 25 gcMarkTinyAllocs() 26 27 // Start The World 28 systemstack(startTheWorldWithSema) 29 } else { 30 ... 31 } 32 }
2. Mark
Mark 阶段是并行的运行,经过在后台一直运行 mark worker 来实现。
1 func gcStart(mode gcMode, forceTrigger bool) { 2 ... 3 //在后台启动 mark worker 4 if mode == gcBackgroundMode { 5 gcBgMarkStartWorkers() 6 } 7 } 8 9 func gcBgMarkStartWorkers() { 10 // Background marking is performed by per-P G's. Ensure that 11 // each P has a background GC G. 12 for _, p := range &allp { 13 if p == nil || p.status == _Pdead { 14 break 15 } 16 if p.gcBgMarkWorker == 0 { 17 go gcBgMarkWorker(p) 18 notetsleepg(&work.bgMarkReady, -1) 19 noteclear(&work.bgMarkReady) 20 } 21 } 22 } 23 // gcBgMarkWorker 是一直在后台运行的,大部分时候是休眠状态,经过 gcController 来调度 24 func gcBgMarkWorker(_p_ *p) { 25 for { 26 // 将当前 goroutine 休眠,直到知足某些条件 27 gopark(...) 28 ... 29 // mark 过程 30 systemstack(func() { 31 // Mark our goroutine preemptible so its stack 32 // can be scanned. This lets two mark workers 33 // scan each other (otherwise, they would 34 // deadlock). We must not modify anything on 35 // the G stack. However, stack shrinking is 36 // disabled for mark workers, so it is safe to 37 // read from the G stack. 38 casgstatus(gp, _Grunning, _Gwaiting) 39 switch _p_.gcMarkWorkerMode { 40 default: 41 throw("gcBgMarkWorker: unexpected gcMarkWorkerMode") 42 case gcMarkWorkerDedicatedMode: 43 gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit) 44 case gcMarkWorkerFractionalMode: 45 gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit) 46 case gcMarkWorkerIdleMode: 47 gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit) 48 } 49 casgstatus(gp, _Gwaiting, _Grunning) 50 }) 51 ... 52 } 53 }
Mark 阶段的标记代码主要在函数 gcDrain() 中实现。
1 // gcDrain scans roots and objects in work buffers, blackening grey 2 // objects until all roots and work buffers have been drained. 3 func gcDrain(gcw *gcWork, flags gcDrainFlags) { 4 ... 5 // Drain root marking jobs. 6 if work.markrootNext < work.markrootJobs { 7 for !(preemptible && gp.preempt) { 8 job := atomic.Xadd(&work.markrootNext, +1) - 1 9 if job >= work.markrootJobs { 10 break 11 } 12 markroot(gcw, job) 13 if idle && pollWork() { 14 goto done 15 } 16 } 17 } 18 19 // 处理 heap 标记 20 // Drain heap marking jobs. 21 for !(preemptible && gp.preempt) { 22 ... 23 //从灰色列队中取出对象 24 var b uintptr 25 if blocking { 26 b = gcw.get() 27 } else { 28 b = gcw.tryGetFast() 29 if b == 0 { 30 b = gcw.tryGet() 31 } 32 } 33 if b == 0 { 34 // work barrier reached or tryGet failed. 35 break 36 } 37 //扫描灰色对象的引用对象,标记为灰色,入灰色队列 38 scanobject(b, gcw) 39 } 40 }
3. Mark termination (STW phase 2)
mark termination 阶段会 stop the world。函数实如今 gcMarkTermination()。1.8 版本已经不会再对 goroutine stack 进行 re-scan 了。细节有点多,这里不细说了。
1 func gcMarkTermination() { 2 // World is stopped. 3 // Run gc on the g0 stack. We do this so that the g stack 4 // we're currently running on will no longer change. Cuts 5 // the root set down a bit (g0 stacks are not scanned, and 6 // we don't need to scan gc's internal state). We also 7 // need to switch to g0 so we can shrink the stack. 8 systemstack(func() { 9 gcMark(startTime) 10 // Must return immediately. 11 // The outer function's stack may have moved 12 // during gcMark (it shrinks stacks, including the 13 // outer function's stack), so we must not refer 14 // to any of its variables. Return back to the 15 // non-system stack to pick up the new addresses 16 // before continuing. 17 }) 18 ... 19 }
7.4 清扫
清扫相对来讲就简单不少了。
1 func gcSweep(mode gcMode) { 2 ... 3 //阻塞式 4 if !_ConcurrentSweep || mode == gcForceBlockMode { 5 // Special case synchronous sweep. 6 ... 7 // Sweep all spans eagerly. 8 for sweepone() != ^uintptr(0) { 9 sweep.npausesweep++ 10 } 11 // Do an additional mProf_GC, because all 'free' events are now real as well. 12 mProf_GC() 13 mProf_GC() 14 return 15 } 16 17 // 并行式 18 // Background sweep. 19 lock(&sweep.lock) 20 if sweep.parked { 21 sweep.parked = false 22 ready(sweep.g, 0, true) 23 } 24 unlock(&sweep.lock) 25 }
对于并行式清扫,在 GC 初始化的时候就会启动 bgsweep(),而后在后台一直循环。
1 func bgsweep(c chan int) { 2 sweep.g = getg() 3 4 lock(&sweep.lock) 5 sweep.parked = true 6 c <- 1 7 goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1) 8 9 for { 10 for gosweepone() != ^uintptr(0) { 11 sweep.nbgsweep++ 12 Gosched() 13 } 14 lock(&sweep.lock) 15 if !gosweepdone() { 16 // This can happen if a GC runs between 17 // gosweepone returning ^0 above 18 // and the lock being acquired. 19 unlock(&sweep.lock) 20 continue 21 } 22 sweep.parked = true 23 goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1) 24 } 25 } 26 27 func gosweepone() uintptr { 28 var ret uintptr 29 systemstack(func() { 30 ret = sweepone() 31 }) 32 return ret 33 }
不论是阻塞式仍是并行式,都是经过 sweepone()函数来作清扫工做的。若是对于上篇文章 Golang 内存管理 熟悉的话,这个地方就很好理解。内存管理都是基于 span 的,mheap_ 是一个全局的变量,全部分配的对象都会记录在 mheap_ 中。在标记的时候,咱们只要找到对对象对应的 span 进行标记,清扫的时候扫描 span,没有标记的 span 就能够回收了。
1 // sweeps one span 2 // returns number of pages returned to heap, or ^uintptr(0) if there is nothing to sweep 3 func sweepone() uintptr { 4 ... 5 for { 6 s := mheap_.sweepSpans[1-sg/2%2].pop() 7 ... 8 if !s.sweep(false) { 9 // Span is still in-use, so this returned no 10 // pages to the heap and the span needs to 11 // move to the swept in-use list. 12 npages = 0 13 } 14 } 15 } 16 17 // Sweep frees or collects finalizers for blocks not marked in the mark phase. 18 // It clears the mark bits in preparation for the next GC round. 19 // Returns true if the span was returned to heap. 20 // If preserve=true, don't return it to heap nor relink in MCentral lists; 21 // caller takes care of it. 22 func (s *mspan) sweep(preserve bool) bool { 23 ... 24 }
7.5 其余
1. gcWork
这里介绍一下任务队列,或者说灰色对象管理。每一个 P 上都有一个 gcw 用来管理灰色对象(get 和 put),gcw 的结构就是 gcWork。gcWork 中的核心是 wbuf1 和 wbuf2,里面存储就是灰色对象,或者说是 work(下面就所有统一叫作 work)。
1 type p struct { 2 ... 3 gcw gcWork 4 } 5 6 type gcWork struct { 7 // wbuf1 and wbuf2 are the primary and secondary work buffers. 8 wbuf1, wbuf2 wbufptr 9 10 // Bytes marked (blackened) on this gcWork. This is aggregated 11 // into work.bytesMarked by dispose. 12 bytesMarked uint64 13 14 // Scan work performed on this gcWork. This is aggregated into 15 // gcController by dispose and may also be flushed by callers. 16 scanWork int64 17 }
既然每一个 P 上有一个 work buffer,那么是否是还有一个全局的 work list 呢?是的。经过在每一个 P 上绑定一个 work buffer 的好处和 cache 同样,不须要加锁。
1 var work struct { 2 full uint64 // lock-free list of full blocks workbuf 3 empty uint64 // lock-free list of empty blocks workbuf 4 pad0 [sys.CacheLineSize]uint8 // prevents false-sharing between full/empty and nproc/nwait 5 ... 6 }
那么为何使用两个 work buffer (wbuf1 和 wbuf2)呢?我下面举个例子。好比我如今要 get 一个 work 出来,先从 wbuf1 中取,wbuf1 为空的话则与 wbuf2 swap 再 get。在其余时间将 work buffer 中的 full 或者 empty buffer 移到 global 的 work 中。这样的好处在于,在 get 的时候去全局的 work 里面取(多个 goroutine 去取会有竞争)。这里有趣的是 global 的 work list 是 lock-free 的,经过原子操做 cas 等实现。下面列举几个函数看一下 gcWrok。
初始化。
1 func (w *gcWork) init() { 2 w.wbuf1 = wbufptrOf(getempty()) 3 wbuf2 := trygetfull() 4 if wbuf2 == nil { 5 wbuf2 = getempty() 6 } 7 w.wbuf2 = wbufptrOf(wbuf2) 8 }
put。
1 // put enqueues a pointer for the garbage collector to trace. 2 // obj must point to the beginning of a heap object or an oblet. 3 func (w *gcWork) put(obj uintptr) { 4 wbuf := w.wbuf1.ptr() 5 if wbuf == nil { 6 w.init() 7 wbuf = w.wbuf1.ptr() 8 // wbuf is empty at this point. 9 } else if wbuf.nobj == len(wbuf.obj) { 10 w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1 11 wbuf = w.wbuf1.ptr() 12 if wbuf.nobj == len(wbuf.obj) { 13 putfull(wbuf) 14 wbuf = getempty() 15 w.wbuf1 = wbufptrOf(wbuf) 16 flushed = true 17 } 18 } 19 20 wbuf.obj[wbuf.nobj] = obj 21 wbuf.nobj++ 22 }
get。
1 // get dequeues a pointer for the garbage collector to trace, blocking 2 // if necessary to ensure all pointers from all queues and caches have 3 // been retrieved. get returns 0 if there are no pointers remaining. 4 //go:nowritebarrier 5 func (w *gcWork) get() uintptr { 6 wbuf := w.wbuf1.ptr() 7 if wbuf == nil { 8 w.init() 9 wbuf = w.wbuf1.ptr() 10 // wbuf is empty at this point. 11 } 12 if wbuf.nobj == 0 { 13 w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1 14 wbuf = w.wbuf1.ptr() 15 if wbuf.nobj == 0 { 16 owbuf := wbuf 17 wbuf = getfull() 18 if wbuf == nil { 19 return 0 20 } 21 putempty(owbuf) 22 w.wbuf1 = wbufptrOf(wbuf) 23 } 24 } 25 26 // TODO: This might be a good place to add prefetch code 27 28 wbuf.nobj-- 29 return wbuf.obj[wbuf.nobj] 30 }
2. forcegc
咱们上面讲了两种 GC 触发方式:自动检测和用户主动调用。除此以后 Golang 自己还会对运行状态进行监控,若是超过两分钟没有 GC,则触发 GC。监控函数是 sysmon(),在主 goroutine 中启动。
1 // The main goroutine 2 func main() { 3 ... 4 systemstack(func() { 5 newm(sysmon, nil) 6 }) 7 } 8 // Always runs without a P, so write barriers are not allowed. 9 func sysmon() { 10 ... 11 for { 12 now := nanotime() 13 unixnow := unixnanotime() 14 15 lastgc := int64(atomic.Load64(&memstats.last_gc)) 16 if gcphase == _GCoff && lastgc != 0 && unixnow-lastgc > forcegcperiod && atomic.Load(&forcegc.idle) != 0 { 17 lock(&forcegc.lock) 18 forcegc.idle = 0 19 forcegc.g.schedlink = 0 20 injectglist(forcegc.g) // 将 forcegc goroutine 加入 runnable queue 21 unlock(&forcegc.lock) 22 } 23 } 24 } 25 26 var forcegcperiod int64 = 2 * 60 *1e9 //两分钟