Go : GC

1、何时触发GC?

当堆上分配大于32k的对象的时候开始检测是否满足GC条件,满足则开始自动GC。

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
shouldhelpgc := false
// 分配的对象小于 32K byte
if size <= maxSmallSize {
    ...
} else {
    shouldhelpgc = true
    ...
}
...
// gcShouldStart() 函数进行触发条件检测
if shouldhelpgc && gcShouldStart(false) {
    // gcStart() 函数进行垃圾回收
    gcStart(gcBackgroundMode, false)
}
}

另外,还可以通过调用runtime.GC()进行主动垃圾回收,主动垃圾回收的过程是主动式的。

// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire
// program.
func GC() {
	gcStart(gcForceBlockMode, false)
}

此外,Golang本身也会对运行状态监控,如果超过2分钟没有Gc,则触发GC。监控函数会在主goroutine中启动

2、GC的触发条件

初始化的时候会设置一个gc的触发阈值,当堆上的活跃对象大于阈值的时候则会触发GC

3、Go的垃圾回收的算法使用的是三色标记法

三色标记法的主要流程:
1、开始时所有对象都为白色。
2、从root开始找可达对象,将其标记为灰色,放入待处理队列。
3、遍历灰色对象队列,灰色对象的引用对象标记为灰色放入待处理队列,同时将自身标为黑色。
4、遍历完灰色队列对象后,清扫白色标记的对象。

三色标记法类似于标记清扫算法。但三色标记的好处在于可以让用户程序和Mark并发执行。

在go中,灰色对象队列使用gcw来管理灰色对象。gcw的结构体是gcWork,gcWork的核心在wbuf1和wbuf2,这里存储的就是灰色对象。

至于为什么采用2个buf来做缓冲呢,官方是这么解释的:

This can be thought of as a stack of both work buffers' 	pointers concatenated. When we pop the last pointer, we shift the stack up by one work buffer by bringing in a new full buffer and discarding an empty one. When we fill both buffers, we shift the stack down by one work buffer by bringing in a new empty buffer and discarding a full one. This way we have one buffer's worth of hysteresis, which amortizes the cost of getting or putting a work buffer over at least one buffer of work and reduces contention on the 	global work lists.

大致意思就是可以认为这2个buf是一个串联在一起的堆栈。在使用的时候就可以分摊开销程本,并且可以减少在global work lists上的竞争。

type p struct {
...
gcw gcWork
}

type gcWork struct {
	// wbuf1 and wbuf2 are the primary and secondary work buffers.
wbuf1, wbuf2 wbufptr

// Bytes marked (blackened) on this gcWork. This is aggregated
// into work.bytesMarked by dispose.
bytesMarked uint64

// Scan work performed on this gcWork. This is aggregated into
// gcController by dispose and may also be flushed by callers.
scanWork int64
}	

4、GC过程

1、STW phase 1
这个阶段主要是在GC开始前做准备工作。

func gcStart(mode gcMode, forceTrigger bool) {
	 //在后台启动 mark worker 
    if mode == gcBackgroundMode {
        gcBgMarkStartWorkers()
}
	...
    if mode == gcBackgroundMode {
    // GC 开始前的准备工作

    //处理设置 GCPhase,setGCPhase 会启动 write barrier
    setGCPhase(_GCmark)
  	
    gcBgMarkPrepare() // Must happen before assist enable.
    gcMarkRootPrepare()

    // Mark all active tinyalloc blocks. Since we're
    // allocating from these, they need to be black like
    // other allocations. The alternative is to blacken
    // the tiny block on every allocation from it, which
    // would slow down the tiny allocator.
    gcMarkTinyAllocs()
  	
    // Start The World
    systemstack(startTheWorldWithSema)
} else {
    ...
}
}

2、Mark

这个阶段是并行的。从程序运行后一直在后台待着,大部分时候是在休眠状态,在gcstart()被调用后gcBgMarkWorker会被启动。

// mark 过程
    systemstack(func() {
    
    .
    // Mark our goroutine preemptible so its stack
    // can be scanned. This lets two mark workers
    // scan each other (otherwise, they would
    // deadlock). We must not modify anything on
    // the G stack. However, stack shrinking is
    // disabled for mark workers, so it is safe to
    // read from the G stack.
    casgstatus(gp, _Grunning, _Gwaiting)
    switch _p_.gcMarkWorkerMode {
    default:
        throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")
    case gcMarkWorkerDedicatedMode:
        gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit)
    case gcMarkWorkerFractionalMode:
        gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
    case gcMarkWorkerIdleMode:
        gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
    }
    casgstatus(gp, _Gwaiting, _Grunning)
    })

从这部分可以看出,当MarkWorker被启动后,会调用gcDrain()函数,由gcDrain实现Mark.

// gcDrain scans roots and objects in work buffers, blackening grey
	// objects until all roots and work buffers have been drained.
	func gcDrain(gcw *gcWork, flags gcDrainFlags) {
	 ...	
  // Drain root marking jobs.
   if work.markrootNext < work.markrootJobs {
    for !(preemptible && gp.preempt) {
        job := atomic.Xadd(&work.markrootNext, +1) - 1
        if job >= work.markrootJobs {
            break
        }
        markroot(gcw, job)
        if idle && po。llWork() {
            goto done
        }
    }
    }

    // 处理 heap 标记
    // Drain heap marking jobs.
    for !(preemptible && gp.preempt) {
    ...
    //从灰色列队中取出对象
    var b uintptr
    if blocking {
        b = gcw.get()
    } else {
        b = gcw.tryGetFast()
        if b == 0 {
            b = gcw.tryGet()
        }
    }
    if b == 0 {
        // work barrier reached or tryGet failed.
        break
    }
    //扫描灰色对象的引用对象,标记为灰色,入灰色队列
    scanobject(b, gcw)
    }
	}

3、Mark termination(Stw phase2)
这个阶段在go的1.8版本后以及不会对goroutine stack进行re-scan了。

4、清扫

清扫有2种方式:阻塞式和并行式
阻塞就是正常跑, 并行式是用锁的方式实现的。

 func gcSweep(mode gcMode) {
	  ...
 //阻塞式
   if !_ConcurrentSweep || mode == gcForceBlockMode {
    // Special case synchronous sweep.
    ...
    // Sweep all spans eagerly.
    for sweepone() != ^uintptr(0) {
        sweep.npausesweep++
    }
    // Do an additional mProf_GC, because all 'free' events are now real as well.
    mProf_GC()
    mProf_GC()
    return
}

    // 并行式
    // Background sweep.
    lock(&sweep.lock)
    if sweep.parked {
    sweep.parked = false
    ready(sweep.g, 0, true)
    }
    unlock(&sweep.lock)
}

go的内存管理都是基于span的,在标记时针对对象的span标记,清扫的时候扫描span,对没标记的span进行回收。

你可能感兴趣的:(从0开始的go语言,golang)