首页 > 其他分享 >Gc 原理

Gc 原理

时间:2023-01-05 19:55:46浏览次数:64  
标签:sched gp 对象 work Gc func 原理 标记

三色标记并不能 并发 或者 增量, 需要 STW 来保证正确性, 想要在并发或者增量的标记算法中保证正确性,我们需要达成以下两种三色不变性(强弱三色不变性) ,采取写屏障来满足.

插入: 满足强 黑色不会 指向白色

  1. 栈上使用,效率不高
  2. 栈上不使用, 我们在扫描结束后, 可能出现 黑色对象引用白色对象的情况,所以我们需要STW,然后重新扫描栈上的对象
  3. image.png

删除:满足弱 (白色对象始终被灰色对象保护)

在GC开始时,会扫描记录整个栈做快照,从而在删除操作时,可以拦截操作,将白色对象置为灰色对象。

image.png
缺点
image.png

混合: 不需要重新扫描栈

栈上的可达对象全部标黑.避免重新扫描灰色对象.

触发时机

  1. 主动调用runtime.GC()
  2. 后台触发, 如果2min没有触发GC,那么就触发一次.image.png
  3. mallocgc()如果去上级取内存,或者分配大对象,我们就要开启垃圾收集.

一个周期的四个阶段

  1. 清理终止阶段;
    1. 暂停程序,所有的处理器在这时会进入安全点(Safe point);(不需要其他线程暂停)
    2. 如果当前垃圾收集循环是强制触发的,我们还需要处理还未被清理的内存管理单元;
  2. 标记阶段;
    1. 将状态切换至 _GCmark、开启写屏障、用户程序协助(Mutator Assists)并将根对象入队;
    2. 恢复执行程序,标记进程和用于协助的用户程序会开始并发标记内存中的对象,写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新创建的对象都会被直接标记成黑色;
    3. 开始扫描根对象,包括所有 Goroutine 的栈、全局对象以及不在堆中的运行时数据结构,扫描 Goroutine 栈期间会暂停当前处理器;
    4. 依次处理灰色队列中的对象,将对象标记成黑色并将它们指向的对象标记成灰色;
    5. 使用分布式的终止算法检查剩余的工作,发现标记阶段完成后进入标记终止阶段;
  3. 标记终止阶段;(GC标记终止:分配黑色,P帮助GC,写入屏障启用)
    1. 清理处理器上的线程缓存;
    2. 暂停程序、将状态切换至 _GCmarktermination 并关闭辅助标记的用户程序;
  4. 清理阶段;
    1. 将状态切换至 _GCoff 开始清理阶段,初始化清理状态并关闭写屏障;
    2. 恢复用户程序,所有新创建的对象会标记成白色;
    3. 后台并发清理所有的内存管理单元,当 Goroutine 申请新的内存管理单元时就会触发清理;

清理终止阶段;

STW 如何暂停以及之后的启动呢?

如何暂停?

所有的 P 都不执行 G, 这就是 暂停 的主要思想!

首先我们会获得worldsema, 这个是个信号量代表一次这有一个人拥有停止世界的权利.
image.png
image.png

  1. sched.gcwait = 1
  2. preemptall() 尽最大努力通知所有P (status != _Prunning)上面的G:你被侵占了,你停下来.
  3. retake 所有 PsyscallP
  4. stop 空闲的 P
  5. 等待 stopwait == 0 (sched.stopwait 如果 > 0 说明还有等待的人)

那么我们在schedule
image.png

func gcstopm() {
	_g_ := getg()
	...
	_p_ := releasep() // p 与 m 解绑
	lock(&sched.lock)
	_p_.status = _Pgcstop // 改变当前 p 的状态
	sched.stopwait--
	if sched.stopwait == 0 {
		notewakeup(&sched.stopnote)
	}
	unlock(&sched.lock)
	stopm()
}
func stopm() {
	_g_ := getg()
	...
	lock(&sched.lock)
	mput(_g_.m) // 将当前的 m 加入 idle list
	unlock(&sched.lock)
	mPark() // 暂停 m ,知道被唤醒
	acquirep(_g_.m.nextp.ptr()) // 被唤醒了,将 nextp 字段的 p 绑定
	_g_.m.nextp = 0
}

如何 重新启动世界?

让所有的 P 开始跑起来.

  1. sched.gcwait = 0
  2. 设置 m.nextp, 然后唤醒, 和上面暂停的stopm对应
  3. wakep建造 M 去绑定 P 去跑任务

如何判别垃圾收集循环是强制触发的?

work.userForced = trigger.kind == **gcTriggerCycle**
用户自己调用: runtime.GC() -> gcStart(gcTrigger{**kind: gcTriggerCycle**, n: n + 1})

标记阶段的相关问题

如何标记?

STW之前: gcStart() -> gcBgMarkStartWorkers() 每个P一个后台标记工作G,如果 **gcBlackenEnabled != 0** (标记阶段),我们 schedule -> findRunnableGCWorker(p)
标记任务G 抽象为一个 **gcBgMarkWorkerNode**

  1. 判断当前 标记root工作是否做完
  2. 标记 堆中的对象
func gcBgMarkWorker() {
	...
	for{
        ...
		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)
            // 扫描 p 的work buf
			switch pp.gcMarkWorkerMode {
			default:
				throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")
			case gcMarkWorkerDedicatedMode:
				gcDrain(&pp.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
				if gp.preempt {
					// We were preempted. This is
					// a useful signal to kick
					// everything out of the run
					// queue so it can run
					// somewhere else.
					if drainQ, n := runqdrain(pp); n > 0 {
						lock(&sched.lock)
						globrunqputbatch(&drainQ, int32(n))
						unlock(&sched.lock)
					}
				}
				// Go back to draining, this time
				// without preemption.
				gcDrain(&pp.gcw, gcDrainFlushBgCredit)
			case gcMarkWorkerFractionalMode:
				gcDrain(&pp.gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit)
			case gcMarkWorkerIdleMode:
				gcDrain(&pp.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
			}
			casgstatus(gp, _Gwaiting, _Grunning)
		})
	}
}
//	灰色对象 变黑
func gcDrain(gcw *gcWork, flags gcDrainFlags) {

	if work.markrootNext < work.markrootJobs {
		for !(gp.preempt && (preemptible || atomic.Load(&sched.gcwaiting) != 0)) {
		    ...
			markroot(gcw, job, flushBgCredit)
			...
		}
	}
	for !(gp.preempt && (preemptible || atomic.Load(&sched.gcwaiting) != 0)) {
		...
		scanobject(b, gcw)
    }

}

如何判别标记结束?

根据work.nwait字段, 我们每一个worker标记P中的workbuf,标记完成就将work.nwait + 1,如果所有的P都被标记完成,也就是= work.nproc时, 我们就标记完成gcMarkDone()

func gcBgMarkWorker() {
	for {
		// Account for time.
		duration := nanotime() - startTime
		gcController.logWorkTime(pp.gcMarkWorkerMode, duration)
		if pp.gcMarkWorkerMode == gcMarkWorkerFractionalMode {
			atomic.Xaddint64(&pp.gcFractionalMarkTime, duration)
		}

		incnwait := atomic.Xadd(&work.nwait, +1) // nwait + 1
		if incnwait > work.nproc {
			println("runtime: p.gcMarkWorkerMode=", pp.gcMarkWorkerMode,
				"work.nwait=", incnwait, "work.nproc=", work.nproc)
			throw("work.nwait > work.nproc")
		}
		pp.gcMarkWorkerMode = gcMarkWorkerNotWorker
    	// 所有的人都等待,那么我们就标记完成
		if incnwait == work.nproc && !gcMarkWorkAvailable(nil) {
			// We don't need the P-local buffers here, allow
			// preemption because we may schedule like a regular
			// goroutine in gcMarkDone (block on locks, etc).
			releasem(node.m.ptr())
			node.m.set(nil)

			// 标记阶段结束, 进入 标记结束阶段
			gcMarkDone()
		}
	}
}

新创建的对象都会被直接标记成黑色 在哪里体现?

// gcBlackenEnabled is 1 if mutator assists and background mark
// workers are allowed to blacken objects. This must only be set when
// gcphase == _GCmark.
var gcBlackenEnabled uint32

gcStart()在标记阶段的 标记tiny 对象结束后将其置为1

如何开启开启写屏障、用户程序协助(Mutator Assists)?

用户程序协助: gcBlackenEnabled字段.
写屏障开启: gcStart() 设置gc 阶段 setGCPhase(_GCmark)

 // 设置 gc phase ,然后 写屏障是否需要( _GCmark ,_GCmarktermination) , 是否允许(需要)
func setGCPhase(x uint32) {
	atomic.Store(&gcphase, x)
	writeBarrier.needed = gcphase == _GCmark || gcphase == _GCmarktermination
	writeBarrier.enabled = writeBarrier.needed || writeBarrier.cgo
}

用户程序协助 是什么呢?

也就是标记辅助

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    ...
    if gcBlackenEnabled != 0 {
    		...
    		assistG.gcAssistBytes -= int64(size)
            // 如果发现不够 借
    		if assistG.gcAssistBytes < 0 {
    			gcAssistAlloc(assistG) // 去 全局贷款
    		}
    	}
    ...
}
func gcAssistAlloc(gp *g) {
	...
retry:
	// 计算 需要 扫描多少
	assistWorkPerByte := gcController.assistWorkPerByte.Load()
	assistBytesPerWork := gcController.assistBytesPerWork.Load()
	debtBytes := -gp.gcAssistBytes
	scanWork := int64(assistWorkPerByte * float64(debtBytes))
	if scanWork < gcOverAssistWork { 
		scanWork = gcOverAssistWork
		debtBytes = int64(assistBytesPerWork * float64(scanWork))
	}

	// 获取全局辅助标记的字节数
	bgScanCredit := atomic.Loadint64(&gcController.bgScanCredit)
	stolen := int64(0)
	if bgScanCredit > 0 {
		if bgScanCredit < scanWork {
			stolen = bgScanCredit
			gp.gcAssistBytes += 1 + int64(assistBytesPerWork*float64(stolen))
		} else {
			stolen = scanWork // 可以 补全
			gp.gcAssistBytes += debtBytes
		}
		// 全局信用扣除stolen点数
		atomic.Xaddint64(&gcController.bgScanCredit, -stolen)
		scanWork -= stolen
    	// 如果借够了
		if scanWork == 0 {
			// We were able to steal all of the credit we
			// needed.
			if traced {
				traceGCMarkAssistDone()
			}
			return
		}
	}

	if trace.enabled && !traced {
		traced = true
		traceGCMarkAssistStart()
	}

	// Perform assist work
	systemstack(func() {
        //  没借够
		gcAssistAlloc1(gp, scanWork) 
        // 如果调用gcDrainN(gcw *gcWork, scanWork int64)完成指定数量的标记任务并返回
	})

	completed := gp.param != nil
	gp.param = nil
	if completed {
		gcMarkDone()
	}

	if gp.gcAssistBytes < 0 {
        // 我们 借 不够, 
		if gp.preempt {
			Gosched()
			goto retry
		}

		//  后台标记 工人如果发现当前的 assist queue 中的 debt 它能还清,还清,然后唤醒
        // 之后我们就会 goto retry
        // 如果还不清的话,我们就还是休眠.
        if !gcParkAssist() {
			goto retry
		}

		// At this point either background GC has satisfied
		// this G's assist debt, or the GC cycle is over.
	}
	
}

混合写屏障的体现?

if writeBarrier.enabled {
  gcWriteBarrier(ptr, val)
} else {
  *ptr = val
}

50BFD5081D5EF9A195B619C2FBE37809.jpg


// gcWriteBarrier performs a heap pointer write and informs the GC.
//
// gcWriteBarrier does NOT follow the Go ABI. It has two WebAssembly parameters:
// R0: the destination of the write (i64)
// R1: the value being written (i64)
TEXT runtime·gcWriteBarrier(SB), NOSPLIT, $16
	// R3 = g.m
	MOVD g_m(g), R3
	// R4 = p
	MOVD m_p(R3), R4
	// R5 = wbBuf.next
	MOVD p_wbBuf+wbBuf_next(R4), R5

	// Record value
	MOVD R1, 0(R5)
	// Record *slot
	MOVD (R0), 8(R5)

	// Increment wbBuf.next
	Get R5
	I64Const $16
	I64Add
	Set R5
	MOVD R5, p_wbBuf+wbBuf_next(R4)

	Get R5
	I64Load (p_wbBuf+wbBuf_end)(R4)
	I64Eq
	If
		// Flush
		MOVD R0, 0(SP)
		MOVD R1, 8(SP)
		CALLNORESUME runtime·wbBufFlush(SB)
	End

	// Do the write
	MOVD R1, (R0)

	RET

如何根对象入队 以及 如何判别根对象?

如何判别根对象?

首先根对象就是指向一个对象的指针,且没有人指向它.
image.png
这些都是根对象.

如何根对象入队

modulesinit 会将 active moudle 赋值
编译的时候,会有元信息放入 moudledata 然后用链表串起来 firstmoduledata

**gcMarkRootPrepare()**将根对象入队. 必须工作在**STW**

  1. 扫描 全局变量**data , bss 段**
  2. 将当前所有的**G**做一份快照(因为我们要扫描它们的**stack**)

func gcMarkRootPrepare() {
	assertWorldStopped()

	// Compute how many data and BSS root blocks there are.
	nBlocks := func(bytes uintptr) int {
		return int(divRoundUp(bytes, rootBlockBytes))
	}

	work.nDataRoots = 0
	work.nBSSRoots = 0

	// Scan globals.
	for _, datap := range activeModules() {
		nDataRoots := nBlocks(datap.edata - datap.data)
		if nDataRoots > work.nDataRoots {
			work.nDataRoots = nDataRoots
		}
	}

	for _, datap := range activeModules() {
		nBSSRoots := nBlocks(datap.ebss - datap.bss)
		if nBSSRoots > work.nBSSRoots {
			work.nBSSRoots = nBSSRoots
		}
	}

	// Scan span roots for finalizer specials.
	//
	// We depend on addfinalizer to mark objects that get
	// finalizers after root marking.
	//
	// We're going to scan the whole heap (that was available at the time the
	// mark phase started, i.e. markArenas) for in-use spans which have specials.
	//
	// Break up the work into arenas, and further into chunks.
	//
	// Snapshot allArenas as markArenas. This snapshot is safe because allArenas
	// is append-only.
	mheap_.markArenas = mheap_.allArenas[:len(mheap_.allArenas):len(mheap_.allArenas)]
	work.nSpanRoots = len(mheap_.markArenas) * (pagesPerArena / pagesPerSpanRoot)

	// 快照
	work.stackRoots = allGsSnapshot()
	work.nStackRoots = len(work.stackRoots)
	....
}

标记终止阶段

  1. 会将 Pwbbuf刷新(因为我们在之前标记阶段,写屏障触发,只有当(wbbuf.next = wbbuf.end时,我们才会刷新),如果发现还有标记工作没有做完,也就是wbbuf.nobj != 0会重试
  2. STW, 唤醒所有 协助 G
  3. 然后进入 标记结束阶段
func gcMarkDone() {
	// Ensure only one thread is running the ragged barrier at a
	// time.
	semacquire(&work.markDoneSema)

top:
	if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil)) {
		semrelease(&work.markDoneSema)
		return
	}

	semacquire(&worldsema)

	gcMarkDoneFlushed = 0
	systemstack(func() {
		gp := getg().m.curg
		casgstatus(gp, _Grunning, _Gwaiting)
		forEachP(func(_p_ *p) {
			wbBufFlush1(_p_) // 刷新 本地缓存 至 the GC work queue.

			_p_.gcw.dispose() // 如果还有工作, 会将 flushedWork 设置为 true
			// Collect the flushedWork flag.
			if _p_.gcw.flushedWork {
				atomic.Xadd(&gcMarkDoneFlushed, 1)
				_p_.gcw.flushedWork = false
			}
		})
		casgstatus(gp, _Gwaiting, _Grunning)
	})
	getg().m.preemptoff = "gcing"
	systemstack(stopTheWorldWithSema) //STW

	...

	atomic.Store(&gcBlackenEnabled, 0) // 此时正常工作
	gcWakeAllAssists()//唤醒所有 协助 G
	schedEnableUser(true) //
	nextTriggerRatio := gcController.endCycle()
	gcMarkTermination(nextTriggerRatio)
}

内存清理

会调用 sweepone(),返回清理的页数,如果是^uintptr(0)就是没有清理的页.

引用

Go语言设计与实现https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/#%E6%B7%B7%E5%90%88%E5%86%99%E5%B1%8F%E9%9A%9C

标签:sched,gp,对象,work,Gc,func,原理,标记
From: https://www.cnblogs.com/jgjg/p/17028731.html

相关文章

  • 在SpringCloud中使用REST服务时的报错
    在SpringCloud中使用REST服务时使用前需要先在配置类中注入RestTemplate的Bean然后再使用自动装配即可@AutowiredprivateRestTemplaterestTemplate;问题......
  • netty中channelHandler实现原理及最佳实践|极客星球
    为持续夯实MobTech袤博科技的数智技术创新能力和技术布道能力,本期极客星球邀请了企业服务研发部工程师梁立从TCP的粘包/半包、Netty处理粘包/半包及源码分析、开源项目......
  • AES加密解密算法原理,以及AES有哪些用途?
    AES加密算法是双向加密,它与单向加密MD5摘要算法不同。我们都是知道双向加密是可逆的,存在密文的密钥,AES算法是现在比较流行的加密算法之一。那么,AES加密解密算法原理是什么,主......
  • AES加密解密算法原理,以及AES有哪些用途?
    AES加密算法是双向加密,它与单向加密MD5摘要算法不同。我们都是知道双向加密是可逆的,存在密文的密钥,AES算法是现在比较流行的加密算法之一。那么,AES加密解密算法原理是什么,......
  • AES加密解密算法原理,以及AES有哪些用途?
    AES加密算法是双向加密,它与单向加密MD5摘要算法不同。我们都是知道双向加密是可逆的,存在密文的密钥,AES算法是现在比较流行的加密算法之一。那么,AES加密解密算法原理是什么,主......
  • RabbitMQ的工作模式及原理(1)
    RabbitMQ的5大核心概念RabbitMQ的5大核心概念:Connection(连接)、Channel(信道)、Exchange(交换机)、Queue(队列)、Virtualhost(虚拟主机)。其中,中间的Broker表示RabbitMQ服务,每个......
  • 【AGC】在云调试删除应用无法再安装问题
    问题背景:俄罗斯cp反馈在AGC平台使用云调试功能出现了问题。复现步骤:安装应用程序->卸载应用程序(长按“删除”按钮)->再次尝试安装。之后收到信息:安装成功。但无论如何......
  • 【AGC】在云调试删除应用无法再安装问题
    问题背景:俄罗斯cp反馈在AGC平台使用云调试功能出现了问题。复现步骤:安装应用程序->卸载应用程序(长按“删除”按钮)->再次尝试安装。之后收到信息:安装成功。但无论如何,已......
  • gcc内置原子操作__sync_系列函数解析
    gcc内置原子操作__sync_系列函数解析gcc4.1.2版本之后,对X86或X86_64支持内置原子操作。就是说,不需要引入第三方库(如pthread)的锁保护,即可对1、2、4、8字节的数值或指针类......
  • [华为SDK]Could not resolve com.huawei.agconnect:agcp:1.6.x.300解决方法
    接入huaweiSDK过程中出现的问题,解决时需要关注项目根路径下的gradle脚本文件这三个地方:首先,华为的maven源需要放置在最前边其次,根gradle依赖项中也需要加入相关依赖最后,记得......