golang源码分析--gc

由于本人也属于小白学习,学习过程中也有很多不解的地方,欢迎大家提问,或者指出我未能讲到的部分,发现gc是一个很庞大的逻辑所以此篇会一直更新,到我觉得真的完全理解了再停更

golang概览及原理

golang的垃圾回收采用的是 标记-清理(Mark-and-Sweep) 算法
就是先标记出需要回收的内存对象快,然后在清理掉;
选取三色标记清除法的原因:
1.对象整理的优势是解决内存碎片问题以及“允许”使用顺序内存分配器。但 Go 运行时的分配算法基于 tcmalloc,基本上没有碎片问题。并且顺序内存分配器在多线程的场景下并不适用。Go 使用的是基于 tcmalloc 的现代内存分配算法,对对象进行整理不会带来实质性的性能提升。
2.分代 GC 依赖分代假设,即 GC 将主要的回收目标放在新创建的对象上(存活时间短,更倾向于被回收),而非频繁检查所有对象。但 Go 的编译器会通过逃逸分析将大部分新生对象存储在栈上(栈直接被回收),只有那些需要长期存在的对象才会被分配到需要进行垃圾回收的堆中。也就是说,分代 GC 回收的那些存活时间短的对象在 Go 中是直接被分配到栈上,当 goroutine 死亡后栈也会被直接回收,不需要 GC 的参与,进而分代假设并没有带来直接优势。并且 Go 的垃圾回收器与用户代码并发执行,使得 STW 的时间与对象的代际、对象的 size 没有关系。Go 团队更关注于如何更好地让 GC 与用户代码并发执行(使用适当的 CPU 来执行垃圾回收),而非减少停顿时间这一单一目标上。
有了GC为什么还会内存泄漏:
原因:预期的能很快被释放的内存由于附着在了长期存活的内存上,或生命期意外的被延长,导致预计能够立即回收的内存长时间得不到回收(由于goroutine还有多种形式)
形式1:预期能被快速释放的内存因被根对象引用而没有得到迅速释放:当有一个全局对象时,可能不经意间将某个变量附着在其上,且忽略的将其进行释放,则该内存永远不会得到释放

var cache = map[interface{}]interface{}{}

func keepalloc() {
  for i := 0; i < 10000; i++ {
    m := make([]byte, 1<<10)
    cache[i] = m
  }
}

形式2:goroutine 泄漏:

Goroutine 作为一种逻辑上理解的轻量级线程,需要维护执行用户代码的上下文信息。在运行过程中也需要消耗一定的内存来保存这类信息,而这些内存在目前版本的 Go 中是不会被释放的。因此,如果一个程序持续不断地产生新的 goroutine、且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象,


func keepalloc2() {
  for i := 0; i < 100000; i++ {
    go func() {
      select {}
    }()
  }
}

这种形式的 goroutine 泄漏还可能由 channel 泄漏导致。而 channel 的泄漏本质上与 goroutine 泄漏存在直接联系。Channel 作为一种同步原语,会连接两个不同的 goroutine,如果一个 goroutine 尝试向一个没有接收方的无缓冲 channel 发送消息,则该 goroutine 会被永久的休眠,整个 goroutine 及其执行栈都得不到释放,


var ch = make(chan struct{})

func keepalloc3() {
  for i := 0; i < 100000; i++ {
    // 没有接收方,goroutine 会一直阻塞
    go func() { ch <- struct{}{} }()
  }
}

gc的触发条件:
1.主动触发,通过调用 runtime.GC 来触发 GC,此调用阻塞式地等待当前 GC 运行完毕。
2.被动触发,分为两种方式:
2.1使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC。
2.2使用步调(Pacing)算法,其核心思想是控制内存增长的比例。
gc优化的手段
1.控制内存分配的速度,限制 goroutine 的数量,从而提高赋值器对 CPU 的利用率。
2.减少并复用内存,例如使用 sync.Pool 来复用需要频繁创建临时对象,例如提前分配足够的内存来降低多余的拷贝。
3.需要时,增大 GOGC 的值,降低 GC 的运行频率。

根据源码go 1.12.9注释GC的流程如下:

gcphase的三个状态
_GCoff // GC not running; sweeping in background, write barrier disabled
_GCmark // GC marking roots and workbufs: allocate black, write barrier ENABLED
_GCmarktermination // GC mark termination: allocate black, P’s help GC, write barrier ENABLED
1.GC执行扫描和终止
a. 暂停整个程序(stop the world),等待所有goroutine到达GC安全点(备注见定义)
b.清除任何未经清除的span,只有在预期时间之前强制执行此GC周期时,才会有未清除的span。
2.GC的标记阶段
a.为了标记阶段将gcphase从_GCoff设置成_GCmark,开启写屏障(详情见备注),启用mutator assist(有所疑问不知作用),将根标记任务放入队列。通过STW保证没有对象会被扫描,直到所有协程(Ps)启用写屏障。
b.唤醒程序(start the world),从此开始,GC的工作由调度程序启用的mark workers和allocation一部分的assists performed执行,写屏障将任何指针指向的新指针和覆盖指针都标记为灰,新申请的对象立即判为黑色。
c.gc执行根标记任务(什么是跟对象?见备注),这包括扫描所有的栈,为所有全局变量标灰色,以及对堆外运行时数据结构中的任何堆指针进行标灰色。扫描栈会停止goroutine,为goroutine指针指向的所有栈着灰色,然后再重启goroutine
d.GC排出灰色对象的工作队列,将每个灰色对象扫描为黑色,并对在该对象中找到的所有指针标记为灰色(这反过来又可能将这些指针添加到工作队列中)。
e.由于 GC work 分散在本地缓存中,因此 GC 使用分布式终止算法来检测何时不再有根标记作业或灰色对象(参见gcMarkDone函数)。此时,GC 状态转换到标记终止(gcMarkTermination)。
3.GC执行标记终止
a.暂停程序(stop the world)
b.设置gcphase状态到_GCmarktermination,停止workers和assists
c.清理工作,如回收mcaches内存
4. GC执行清除阶段
a.设置gcphase到_GCoff,设置清除状态
并禁止写屏障。

b.唤醒程序(start the world),从此时开始,新申请的对象为白色,若必要可以在使用前清扫spans。
c.gc在后台执行回收白色对象并响应内存的分配
5.当内存分配足够多,则从1再开始

golang中GC的触发时机:

1.gcTriggerAlways: 强制触发GC
2.gcTriggerHeap: 当前分配的内存达到一定阈值时触发,这个阈值在每次GC过后都会根据堆内存的增长情况3.和CPU占用率来调整
4.gcTriggerTime: 当一定时间没有执行过GC就触发GC(2分钟)
5.gcTriggerCycle: runtime.GC()调用

//GC函数是用我们可以主动开启GC的一个函数
//GC函数流程:一个流程,清除终结,标记,标记终结,清除
//1.在循环N中,清除终止,标记或者标记终止,请等待到标记终止过渡到清除N
//2.在清除N中,我们完成清除,在这个节点我们可以开启第N+1个完整周期
//3.通过清除终结N+1来触发N+1循环
//4.等待N+1的标记结束
//5.帮助清除N+1直到结束
func GC() {
    //获取当前循环
	n := atomic.Load(&work.cycles)
	//gcWaitOnMark一直阻塞,直到GC完成第N个标记阶段。 如果GC已完成此标记阶段,它将立即返回。
	gcWaitOnMark(n)

	// 因为gcphase状态不为_GCmark
	gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})

	// Wait for mark termination N+1 to complete.
	gcWaitOnMark(n + 1)

	// Finish sweep N+1 before returning. We do this both to
	// complete the cycle and because runtime.GC() is often used
	// as part of tests and benchmarks to get the system into a
	// relatively stable and isolated state.
	for atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) {
		sweep.nbgsweep++
		Gosched()
	}

	// Callers may assume that the heap profile reflects the
	// just-completed cycle when this returns (historically this
	// happened because this was a STW GC), but right now the
	// profile still reflects mark termination N, not N+1.
	//
	// As soon as all of the sweep frees from cycle N+1 are done,
	// we can go ahead and publish the heap profile.
	//
	// First, wait for sweeping to finish. (We know there are no
	// more spans on the sweep queue, but we may be concurrently
	// sweeping spans, so we have to wait.)
	for atomic.Load(&work.cycles) == n+1 && atomic.Load(&mheap_.sweepers) != 0 {
		Gosched()
	}

	// Now we're really done with sweeping, so we can publish the
	// stable heap profile. Only do this if we haven't already hit
	// another mark termination.
	mp := acquirem()
	cycle := atomic.Load(&work.cycles)
	if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) {
		mProf_PostSweep()
	}
	releasem(mp)
}

gcStart源代码,gcStart的代码对应的是真正的gc流程

//开启GC流程,将gcphase的状态从_GCoff设置到_GCmark或者执行所有的GC流程
func gcStart(trigger gcTrigger) {
	// 检查执行条件
	mp := acquirem()
	if gp := getg(); gp == mp.g0 || mp.locks > 1 || mp.preemptoff != "" {
		releasem(mp)
		return
	}
	releasem(mp)
	mp = nil
	//清除任何未经清除的span
	for trigger.test() && sweepone() != ^uintptr(0) {
		sweep.nbgsweep++
	}


	//开启第一阶段,通过work中的标记指代不同的阶段
	semacquire(&work.startSema)
	// Re-check transition condition under transition lock.
	if !trigger.test() {
		semrelease(&work.startSema)
		return
	}
	// For stats, check if this GC was forced by the user.
	work.userForced = trigger.kind == gcTriggerAlways || trigger.kind == gcTriggerCycle
	// 根据当前gc的gcstoptheworld运行状态的不同指定mode
	//目的是防止触发器使得多个gorooutine启用多个STW
	mode := gcBackgroundMode//
	if debug.gcstoptheworld == 1 {
		mode = gcForceMode
	} else if debug.gcstoptheworld == 2 {
		mode = gcForceBlockMode
	}
	//加锁,STW的第一步
	semacquire(&worldsema)

	if trace.enabled {
		traceGCStart()
	}
	// 检测所有goroutine已经到安全状态并回收缓存
	for _, p := range allp {
		if fg := atomic.Load(&p.mcache.flushGen); fg != mheap_.sweepgen {
			println("runtime: p", p.id, "flushGen", fg, "!= sweepgen", mheap_.sweepgen)
			throw("p mcache not flushed")
		}
	}
    //标记之前的准备工作。注意gcBgMarkStartWorkers
   //gcBgMarkStartWorkers准备后台标记工作进程goroutines。
   //这些goroutine在mark阶段之前不会运行,但必须在工作未停止时从常规G堆栈启动它们。呼叫者必须持有worldsema
   //特别注意,此函数后台worker程序会执行gcphase状态到_GCmarktermination,判断标记结束
   //也会置gcphase到_GCoff,设置清除状态并禁止写屏障
	gcBgMarkStartWorkers()
    // gcResetMarkState重置标记之前的全局状态(并发或STW),并重置所有G的堆栈扫描状态。
    //这是安全的,无需停止世界,因为在此期间或之后创建的所有G都会以重置状态开始。 
	gcResetMarkState()

	work.stwprocs, work.maxprocs = gomaxprocs, gomaxprocs
	if work.stwprocs > ncpu {
		// This is used to compute CPU time of the STW phases,
		// so it can't be more than ncpu, even if GOMAXPROCS is.
		work.stwprocs = ncpu
	}
	work.heap0 = atomic.Load64(&memstats.heap_live)
	work.pauseNS = 0
	work.mode = mode

	now := nanotime()
	work.tSweepTerm = now
	work.pauseStart = now
	if trace.enabled {
		traceGCSTWStart(1)
	}
	//****************************************
	//STW的核心实现,到这里为止完成了STW
	//****************************************
	systemstack(stopTheWorldWithSema)
	// Finish sweep before we start concurrent scan.
	systemstack(func() {
		finishsweep_m()
	})
	// clearpools before we start the GC. If we wait they memory will not be
	// reclaimed until the next GC cycle.
	clearpools()
    //开启下一个循环
	work.cycles++

	gcController.startCycle()
	work.heapGoal = memstats.next_gc

	// In STW mode, disable scheduling of user Gs. This may also
	// disable scheduling of this goroutine, so it may block as
	// soon as we start the world again.
	if mode != gcBackgroundMode {
		schedEnableUser(false)
	}
    //转换gcphase的状态,进入当前标记阶段,并开启写屏障
 	setGCPhase(_GCmark)
 	//为标记做准备
	gcBgMarkPrepare() // Must happen before assist enable.
	//gcMarkRootPrepare将根扫描作业(堆栈、全局和一些其他)排队,并初始化扫描相关状态。
	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()

	// At this point all Ps have enabled the write
	// barrier, thus maintaining the no white to
	// black invariant. Enable mutator assists to
	// put back-pressure on fast allocating
	// mutators.
	atomic.Store(&gcBlackenEnabled, 1)

	// Assists and workers can start the moment we start
	// the world.
	//statr world
	gcController.markStartTime = now

	// Concurrent mark.
	systemstack(func() {
		now = startTheWorldWithSema(trace.enabled)
		work.pauseNS += now - work.pauseStart
		work.tMark = now
	})
	// In STW mode, we could block the instant systemstack
	// returns, so don't do anything important here. Make sure we
	// block rather than returning to user code.
	if mode != gcBackgroundMode {
		Gosched()
	}

	semrelease(&work.startSema)
}

备注
安全点:程序执行期间的一个点,在此点上所有GC根都是已知的,并且所有堆对象内容都是一致的。从全局的角度来看,所有线程都必须在安全点阻塞,然后GC才能运行。
写屏障:https://segmentfault.com/a/1190000012597428,此链接中有例子,将写屏障讲的十分准确
根对象:它是垃圾回收器在标记过程时最先检查的对象,包括:
全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
tcmalloc:核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。

参考文章:
Go GC 20 问

发布了212 篇原创文章 · 获赞 33 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/hello_bravo_/article/details/103786191