golang的垃圾回收算法之三调度策略

发布时间:2022-12-21 17:30

一、GC的调度策略

做事前先确定方法,GC也是如此。什么时候儿进行GC,GC后如何结束。有始有终,方成正果。在golang的GC启动有三种策略:
1、按堆大小来确定是否启动GC,即前文提到的到达指定阈值。
2、定时启动GC,这个好理解,每隔一段时间(默认2分钟)如果没有GC就执行一次。
3、循环处理GC,如果没有启动GC,则进入下一轮。

Go通过内存池技术来管理内存的分配(这是一种流行病),为了更好的适应对内存的管理需求,采用了和 CPU缓存同样的设计,分成了三类:线程单独的缓存mcache、中心缓存 mcentral (管理Span)、堆页 mheap ,而上面的三种策略其实也和这三类缓存有着各种联系。
Golang对象在进行内存分配的时候,通常会根据大小划分为微小对象、小对象和大对象三类,它分别对应着三种分配方式,即调用 tiny malloc、small alloc、large alloc三类内存分配函数。一般来说,mcache 负责tiny malloc、small alloc 的分配,妆 mcache 中没有空闲内存块也即无法分配内存时,就需要mcentral 或 mheap 来分配内存,而此时会尝试触发 GC; large alloc 在在堆页上分配内存,所以必然会启动尝试GC。

二、代码分析

老规矩,先看代码:

//mgc.go
// gcShouldStart returns true if the exit condition for the _GCoff
// phase has been met. The exit condition should be tested when
// allocating.
//
// If forceTrigger is true, it ignores the current heap size, but
// checks all other conditions. In general this should be false.
func gcShouldStart(forceTrigger bool) bool {
	return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0
}
// startCycle resets the GC controller's state and computes estimates
// for a new GC cycle. The caller must hold worldsema.
func (c *gcControllerState) startCycle() {
	c.scanWork = 0
	c.bgScanCredit = 0
	c.assistTime = 0
	c.dedicatedMarkTime = 0
	c.fractionalMarkTime = 0
	c.idleMarkTime = 0

	// If this is the first GC cycle or we're operating on a very
	// small heap, fake heap_marked so it looks like gc_trigger is
	// the appropriate growth from heap_marked, even though the
	// real heap_marked may not have a meaningful value (on the
	// first cycle) or may be much smaller (resulting in a large
	// error response).
	if memstats.gc_trigger <= heapminimum {
		memstats.heap_marked = uint64(float64(memstats.gc_trigger) / (1 + c.triggerRatio))
	}

	// Re-compute the heap goal for this cycle in case something
	// changed. This is the same calculation we use elsewhere.
	memstats.next_gc = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
	if gcpercent < 0 {
		memstats.next_gc = ^uint64(0)
	}

	// Ensure that the heap goal is at least a little larger than
	// the current live heap size. This may not be the case if GC
	// start is delayed or if the allocation that pushed heap_live
	// over gc_trigger is large or if the trigger is really close to
	// GOGC. Assist is proportional to this distance, so enforce a
	// minimum distance, even if it means going over the GOGC goal
	// by a tiny bit.
	if memstats.next_gc < memstats.heap_live+1024*1024 {
		memstats.next_gc = memstats.heap_live + 1024*1024
	}

	// Compute the total mark utilization goal and divide it among
	// dedicated and fractional workers.
	totalUtilizationGoal := float64(gomaxprocs) * gcGoalUtilization
	c.dedicatedMarkWorkersNeeded = int64(totalUtilizationGoal)
	c.fractionalUtilizationGoal = totalUtilizationGoal - float64(c.dedicatedMarkWorkersNeeded)
	if c.fractionalUtilizationGoal > 0 {
		c.fractionalMarkWorkersNeeded = 1
	} else {
		c.fractionalMarkWorkersNeeded = 0
	}

	// Clear per-P state
	for _, p := range &allp {
		if p == nil {
			break
		}
		p.gcAssistTime = 0
	}

	// Compute initial values for controls that are updated
	// throughout the cycle.
	c.revise()

	if debug.gcpacertrace > 0 {
		print("pacer: assist ratio=", c.assistWorkPerByte,
			" (scan ", memstats.heap_scan>>20, " MB in ",
			work.initialHeapLive>>20, "->",
			memstats.next_gc>>20, " MB)",
			" workers=", c.dedicatedMarkWorkersNeeded,
			"+", c.fractionalMarkWorkersNeeded, "\n")
	}
}

还有一个强制启动forcegchelper在上一篇提到过(Time-triggered),这里不再重复拷贝。在proc.go中有一段辅助代码:

// start forcegc helper goroutine
func init() {
	go forcegchelper()
}
// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9

// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
	// If a heap span goes unused for 5 minutes after a garbage collection,
	// we hand it back to the operating system.
	scavengelimit := int64(5 * 60 * 1e9)

	if debug.scavenge > 0 {
		// Scavenge-a-lot for testing.
		forcegcperiod = 10 * 1e6
		scavengelimit = 20 * 1e6
	}
......
}

这里首先要介绍 Golang 中的三个基本的概念:G, M, P即:
G: Goroutine 执行的上下文环境。
M: 操作系统线程。
P: Processer。进程调度的关键,调度器,也可以认为约等于CPU。
一个 Goroutine 的运行需要G+P+M三部分结合起来。知道了这些,你再看源码注释中,的P,M之类的才不会蒙圈,基本的术语还是清楚明白。上面的代码注释清晰,这里不再赘述,对照着前面的说明就可以明白这些。

三、总结

GC是一个系统工程,从每个细节的设计开始,就意味着一个互相依赖、互相影响甚至互相制约的工程出现了。正如在前面反复强调的,一个GC依靠一种算法包打天下是不可能的,那么综合设计、平衡调度就是一种最现实的实现方式。这既是对实际情况的一种最优处理,也是对实际应用的一种妥协。
硬件和软件不断在进步,希望能有更好的策略和更好的算法出现。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号