网站seo推广营销,网络公司代做的网站注意事项,众筹网站平台建设,中企动力 集团网站1. Golang GC 发展 Golang 从第一个版本以来#xff0c;GC 一直是大家诟病最多的。但是每一个版本的发布基本都伴随着 GC 的改进。下面列出一些比较重要的改动。 v1.1 STWv1.3 Mark STW, Sweep 并行v1.5 三色标记法v1.8 hybrid write barrier2. GC 算法简介 这一小节介绍三…1. Golang GC 发展 Golang 从第一个版本以来GC 一直是大家诟病最多的。但是每一个版本的发布基本都伴随着 GC 的改进。下面列出一些比较重要的改动。 v1.1 STWv1.3 Mark STW, Sweep 并行v1.5 三色标记法v1.8 hybrid write barrier2. GC 算法简介 这一小节介绍三种经典的 GC 算法 引用计数reference counting标记-清扫mark sweep节点复制Copying Garbage Collection分代收集Generational Garbage Collection 3. 引用计数 引用计数的思想非常简单每个单元维护一个域保存其它单元指向它的引用数量类似有向图的入度。当引用数量为 0 时将其回收。引用计数是渐进式的能够将内存管理的开销分布到整个程序之中。C 的 share_ptr 使用的就是引用计算方法。 引用计数算法实现一般是把所有的单元放在一个单元池里比如类似 free list。这样所有的单元就被串起来了就可以进行引用计数了。新分配的单元计数值被设置为 1注意不是 0因为申请一般都说 ptr new object 这种。每次有一个指针被设为指向该单元时该单元的计数值加 1而每次删除某个指向它的指针时它的计数值减 1。当其引用计数为 0 的时候该单元会被进行回收。虽然这里说的比较简单实现的时候还是有很多细节需要考虑比如删除某个单元的时候那么它指向的所有单元都需要对引用计数减 1。那么如果这个时候发现其中某个指向的单元的引用计数又为 0那么是递归的进行还是采用其他的策略呢递归处理的话会导致系统颠簸。关于这些细节这里就不讨论了可以参考文章后面的给的参考资料。 优点 渐进式。内存管理与用户程序的执行交织在一起将 GC 的代价分散到整个程序。不像标记-清扫算法需要 STW (Stop The WorldGC 的时候挂起用户程序)。算法易于实现。内存单元能够很快被回收。相比于其他垃圾回收算法堆被耗尽或者达到某个阈值才会进行垃圾回收。缺点 原始的引用计数不能处理循环引用。大概这是被诟病最多的缺点了。不过针对这个问题也除了很多解决方案比如强引用等。维护引用计数降低运行效率。内存单元的更新删除等都需要维护相关的内存单元的引用计数相比于一些追踪式的垃圾回收算法并不需要这些代价。单元池 free list 实现的话不是 cache-friendly 的这样会导致频繁的 cache miss降低程序运行效率。4. 标记-清扫 标记-清扫算法是第一种自动内存管理基于追踪的垃圾收集算法。算法思想在 70 年代就提出了是一种非常古老的算法。内存单元并不会在变成垃圾立刻回收而是保持不可达状态直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序也就是 STW转而执行垃圾回收程序。垃圾回收程序对所有的存活单元进行一次全局遍历确定哪些单元可以回收。算法分两个部分标记mark和清扫sweep。标记阶段表明所有的存活单元清扫阶段将垃圾单元回收。可视化可以参考下图。 标记-清扫算法的优点也就是基于追踪的垃圾回收算法具有的优点避免了引用计数算法的缺点不能处理循环引用需要维护指针。缺点也很明显需要 STW。 三色标记算法 三色标记算法是对标记阶段的改进原理如下 起初所有对象都是白色。从根出发扫描所有可达对象标记为灰色放入待处理队列。从队列取出灰色对象将其引用对象标记为灰色放入队列自身标记为黑色。重复 3直到灰色对象队列为空。此时白色对象即为垃圾进行回收。 可视化如下。 三色标记的一个明显好处是能够让用户程序和 mark 并发的进行具体可以参考论文《On-the-fly garbage collection: an exercise in cooperation.》。Golang 的 GC 实现也是基于这篇论文后面再具体说明。 5. 节点复制 节点复制也是基于追踪的算法。其将整个堆等分为两个半区semi-space一个包含现有数据另一个包含已被废弃的数据。节点复制式垃圾收集从切换flip两个半区的角色开始然后收集器在老的半区也就是 Fromspace 中遍历存活的数据结构在第一次访问某个单元时把它复制到新半区也就是 Tospace 中去。在 Fromspace 中所有存活单元都被访问过之后收集器在 Tospace 中建立一个存活数据结构的副本用户程序可以重新开始运行了。 优点 所有存活的数据结构都缩并地排列在 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 : false4 // 分配的对象小于 32K byte5 if size maxSmallSize {6 ...7 } else {8 shouldhelpgc true9 ...
10 }
11 ...
12 // gcShouldStart() 函数进行触发条件检测
13 if shouldhelpgc gcShouldStart(false) {
14 // gcStart() 函数进行垃圾回收
15 gcStart(gcBackgroundMode, false)
16 }
17 } View Code 上面是自动垃圾回收还有一种是主动垃圾回收通过调用 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 } View Code 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 _GCoff2 // phase has been met. The exit condition should be tested when3 // allocating.4 //5 // If forceTrigger is true, it ignores the current heap size, but6 // 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 09 }
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 } View Code 3. 垃圾回收的主要流程 三色标记法主要流程如下 所有对象最开始都是白色。从 root 开始找到所有可达对象标记为灰色放入待处理队列。遍历灰色对象队列将其引用对象标记为灰色放入待处理队列自身标记为黑色。处理完灰色对象队列执行清扫工作。 详细的过程如下图所示具体可参考 [9]。 关于上图有几点需要说明的是。 首先从 root 开始遍历root 包括全局指针和 goroutine 栈上的指针。mark 有两个过程。从 root 开始遍历标记为灰色。遍历灰色队列。re-scan 全局指针和栈。因为 mark 和用户程序是并行的所以在过程 1 的时候可能会有新的对象分配这个时候就需要通过写屏障write barrier记录下来。re-scan 再完成检查一下。Stop The World 有两个过程。第一个是 GC 将要开始的时候这个时候主要是一些准备工作比如 enable write barrier。第二个过程就是上面提到的 re-scan 过程。如果这个时候没有 stw那么 mark 将无休止。 另外针对上图各个阶段对应 GCPhase 如下 Off: _GCoffStack scan ~ Mark: _GCmarkMark termination: _GCmarktermination 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 } View Code 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 World9 systemstack(stopTheWorldWithSema)
10 ...
11 if mode gcBackgroundMode {
12 // GC 开始前的准备工作
13
14 //处理设置 GCPhasesetGCPhase 还会 enable write barrier
15 setGCPhase(_GCmark)
16
17 gcBgMarkPrepare() // Must happen before assist enable.
18 gcMarkRootPrepare()
19
20 // Mark all active tinyalloc blocks. Since were
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 } View Code 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 Gs. 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 } View Code Mark 阶段的标记代码主要在函数 gcDrain() 中实现。 1 // gcDrain scans roots and objects in work buffers, blackening grey2 // 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) - 19 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 } View Code 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 stack4 // were currently running on will no longer change. Cuts5 // the root set down a bit (g0 stacks are not scanned, and6 // we dont need to scan gcs internal state). We also7 // need to switch to g0 so we can shrink the stack.8 systemstack(func() {9 gcMark(startTime)
10 // Must return immediately.
11 // The outer functions stack may have moved
12 // during gcMark (it shrinks stacks, including the
13 // outer functions 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 } View Code 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 } View Code 对于并行式清扫在 GC 初始化的时候就会启动 bgsweep()然后在后台一直循环。 1 func bgsweep(c chan int) {2 sweep.g getg()3 4 lock(sweep.lock)5 sweep.parked true6 c - 17 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 } View Code 不管是阻塞式还是并行式都是通过 sweepone()函数来做清扫工作的。如果对于上篇文章 Golang 内存管理 熟悉的话这个地方就很好理解。内存管理都是基于 span 的mheap_ 是一个全局的变量所有分配的对象都会记录在 mheap_ 中。在标记的时候我们只要找到对对象对应的 span 进行标记清扫的时候扫描 span没有标记的 span 就可以回收了。 1 // sweeps one span2 // returns number of pages returned to heap, or ^uintptr(0) if there is nothing to sweep3 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 preservetrue, dont 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 } View Code 7.5 其他 1. gcWork 这里介绍一下任务队列或者说灰色对象管理。每个 P 上都有一个 gcw 用来管理灰色对象get 和 putgcw 的结构就是 gcWork。gcWork 中的核心是 wbuf1 和 wbuf2里面存储就是灰色对象或者说是 work下面就全部统一叫做 work。 1 type p struct {2 ...3 gcw gcWork4 }5 6 type gcWork struct {7 // wbuf1 and wbuf2 are the primary and secondary work buffers.8 wbuf1, wbuf2 wbufptr9
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 } View Code 既然每个 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 } View Code 那么为什么使用两个 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 } View Code 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 } View Code get。 1 // get dequeues a pointer for the garbage collector to trace, blocking2 // if necessary to ensure all pointers from all queues and caches have3 // been retrieved. get returns 0 if there are no pointers remaining.4 //go:nowritebarrier5 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 } View Code 2. forcegc 我们上面讲了两种 GC 触发方式自动检测和用户主动调用。除此之后 Golang 本身还会对运行状态进行监控如果超过两分钟没有 GC则触发 GC。监控函数是 sysmon()在主 goroutine 中启动。 1 // The main goroutine2 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 //两分钟 View Code 转载于:https://www.cnblogs.com/hezhixiong/p/9577199.html