golang gc
参考文章:垃圾回收器
因为三色标记以及混合写屏障在 Go 中的源码实现,笔者目前尚未理解清楚,所以本文省略了该部分内容。后续有时间研究明白了再补上~
并发三色标记垃圾回收
算法思想可参考:Golang’s Real-time GC in Theory and Practice 和 Golang’s realtime garbage collector
然而在实际实现上却有些变化,例如网上很多文章都说在启用写屏障的情况下,新创建的对象都标记为灰色,但笔者在 go 1.13 源码中,func gcmarknewobject(obj, size, scanSize uintptr)
方法被 mallocgc 调用,其注释写明了:gcmarknewobject marks a newly allocated object black. obj must not contain any non-nil pointers。
Go 运行时的垃圾回收分四个阶段:
- 准备阶段:STW,初始化标记任务,启用写屏障
- 标记阶段 GCMark:标记存活对象,并发与用户代码执行,保持只占用25%CPU
- 标记终止阶段 GCMarkTermination:STW,关闭写屏障
- 清扫阶段 GCOff:回收白色对象,并发与用户代码执行
GC 初始化
在 Go 运行时初始化 schedinit() 方法中会调用 gcinit()。
func gcinit() {
if unsafe.Sizeof(workbuf{}) != _WorkbufSize {
throw("size of Workbuf is suboptimal")
}
// No sweep on the first cycle.
mheap_.sweepdone = 1
// Set a reasonable initial GC trigger.
memstats.triggerRatio = 7 / 8.0
// Fake a heap_marked value so it looks like a trigger at
// heapminimum is the appropriate growth from heap_marked.
// This will go into computing the initial GC goal.
// 初始化计算 GC 目标所需要的参数
memstats.heap_marked = uint64(float64(heapminimum) / (1 + memstats.triggerRatio))
// Set gcpercent from the environment. This will also compute
// and set the GC trigger and goal.
_ = setGCPercent(readgogc())
work.startSema = 1
work.markDoneSema = 1
}
func setGCPercent(in int32) (out int32) {
// Run on the system stack since we grab the heap lock.
systemstack(func() {
lock(&mheap_.lock)
out = gcpercent
if in < 0 {
in = -1
}
gcpercent = in
// 计算触发GC的最小堆大小,默认4M
heapminimum = defaultHeapMinimum * uint64(gcpercent) / 100
// Update pacing in response to gcpercent change.
// 计算触发GC的阈值,GC目标,标记的步调,清扫的步调,释放内存回操作系统的步调
gcSetTriggerRatio(memstats.triggerRatio)
unlock(&mheap_.lock)
})
// If we just disabled GC, wait for any concurrent GC mark to
// finish so we always return with no GC running.
if in < 0 {
gcWaitOnMark(atomic.Load(&work.cycles))
}
return out
}
gcSetTriggerRatio 中关于 GC 相关参数的计算:
- gcpercent:百分比参数,用于调整 GC 目标(memstats.next_gc)和触发阈值(memstats.triggerRatio),默认为100
- memstats.heap_marked:上一轮 GC 结束后,还在使用的堆内存
- memstats.heap_alive:从 mcentral 分配出去的小对象堆内存 + mheap.free 分配出去的大对象堆内存
- memstats.next_gc:当下一次 GC 结束后,堆内存的目标,max(minTrigger, memstats.heap_marked*(1+memstats.triggerRatio), memstats.heap_marked*(1+gcpercent/100))
- memstats.gc_trigger:触发 GC 的堆内存阈值,max(minTrigger, memstats.heap_marked*(1+memstats.triggerRatio))
- memstats.triggerRatio:初始化为 min(7/8, 0.95*gcpercent/100),即0.875,会在每次 GC 标记结束时被 gcControllerState.endCycle 更新
如果当前正在执行 GC,gcSetTriggerRatio 会调用 gcController.revise 调整标记的步调,如果 memstats.heap_live <= memstats.next_gc,则 gcAssistAlloc 需要做的工作 scanWork 也会越少,反之越多。
gcSetTriggerRatio 还会设置 mheap_.sweepPagesPerByte,当 memstats.heap_live 越接近 memstats.gc_trigger,mheap_.sweepPagesPerByte 的值越大,则在创建新对象时,需要清扫的工作越多,反之越少。
gcSetTriggerRatio 还会调用 gcPaceScavenger,判断是否达到阈值(memstats.heap_sys - memstats.heap_released > 1.1 * memstats.next_gc),如果达到,则计算 mheap_.scavengeBytesPerNS,并唤醒 scavenge 协程,最终调用 mheap.scavengeLocked,释放 mheap.free 中空闲的 span。
GC 开始
触发 GC 的时机
GC 并发标记
辅助标记是为了防止 GC 期间,程序不断分配大量内存,导致 GC 标记时间过长或者无法结束标记。
GC 标记结束
GC 并发清扫
清扫过程是为了回收白色对象,辅助清扫是为了避免下次GC启动时做过多的清扫工作。