三色标记并不能 并发 或者 增量, 需要 STW 来保证正确性, 想要在并发或者增量的标记算法中保证正确性,我们需要达成以下两种三色不变性(强弱三色不变性) ,采取写屏障来满足.
插入: 满足强 黑色不会 指向白色
- 栈上使用,效率不高
- 栈上不使用, 我们在扫描结束后, 可能出现 黑色对象引用白色对象的情况,所以我们需要STW,然后重新扫描栈上的对象
删除:满足弱 (白色对象始终被灰色对象保护)
在GC开始时,会扫描记录整个栈做快照,从而在删除操作时,可以拦截操作,将白色对象置为灰色对象。
缺点
混合: 不需要重新扫描栈
栈上的可达对象全部标黑.避免重新扫描灰色对象.
触发时机
- 主动调用
runtime.GC()
- 后台触发, 如果
2min
没有触发GC
,那么就触发一次. mallocgc()
如果去上级取内存,或者分配大对象,我们就要开启垃圾收集.
一个周期的四个阶段
- 清理终止阶段;
- 暂停程序,所有的处理器在这时会进入安全点(Safe point);(不需要其他线程暂停)
- 如果当前垃圾收集循环是强制触发的,我们还需要处理还未被清理的内存管理单元;
- 标记阶段;
- 将状态切换至 _GCmark、开启写屏障、用户程序协助(Mutator Assists)并将根对象入队;
- 恢复执行程序,标记进程和用于协助的用户程序会开始并发标记内存中的对象,写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新创建的对象都会被直接标记成黑色;
- 开始扫描根对象,包括所有 Goroutine 的栈、全局对象以及不在堆中的运行时数据结构,扫描 Goroutine 栈期间会暂停当前处理器;
- 依次处理灰色队列中的对象,将对象标记成黑色并将它们指向的对象标记成灰色;
- 使用分布式的终止算法检查剩余的工作,发现标记阶段完成后进入标记终止阶段;
- 标记终止阶段;(GC标记终止:分配黑色,P帮助GC,写入屏障启用)
- 清理处理器上的线程缓存;
- 暂停程序、将状态切换至 _GCmarktermination 并关闭辅助标记的用户程序;
- 清理阶段;
- 将状态切换至 _GCoff 开始清理阶段,初始化清理状态并关闭写屏障;
- 恢复用户程序,所有新创建的对象会标记成白色;
- 后台并发清理所有的内存管理单元,当 Goroutine 申请新的内存管理单元时就会触发清理;
清理终止阶段;
STW 如何暂停以及之后的启动呢?
如何暂停?
所有的 P 都不执行 G, 这就是 暂停 的主要思想!
首先我们会获得worldsema
, 这个是个信号量代表一次这有一个人拥有停止世界的权利.
sched.gcwait = 1
preemptall()
尽最大努力通知所有P (status != _Prunning)
上面的G:你被侵占了,你停下来.retake
所有Psyscall
的P
stop
空闲的P
等待 stopwait == 0
(sched.stopwait
如果 > 0 说明还有等待的人)
那么我们在schedule
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 开始跑起来.
sched.gcwait = 0
- 设置
m.nextp
, 然后唤醒, 和上面暂停的stopm
对应 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**
- 判断当前
标记root工作是否做完
- 标记 堆中的对象
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
}
// 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
如何根对象入队 以及 如何判别根对象?
如何判别根对象?
首先根对象就是指向一个对象的指针,且没有人指向它.
这些都是根对象.
如何根对象入队
modulesinit 会将 active moudle 赋值
编译的时候,会有元信息放入 moudledata 然后用链表串起来 firstmoduledata
由 **gcMarkRootPrepare()**
将根对象入队. 必须工作在**STW**
- 扫描 全局变量
**data , bss 段**
- 将当前所有的
**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)
....
}
标记终止阶段
- 会将
P
的wbbuf
刷新(因为我们在之前标记阶段,写屏障触发,只有当(wbbuf.next = wbbuf.end
时,我们才会刷新),如果发现还有标记工作没有做完,也就是wbbuf.nobj != 0
会重试 STW
, 唤醒所有 协助 G- 然后进入 标记结束阶段
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