Golang中的切片与GC

今天再看 timer 源码的时候,在函数 clearDeletedTimers() 里看到一段对切片的处理代码,实现目的就是对一个切片内容进行缩容。

// src/runtime/time.go

// The caller must have locked the timers for pp.
func clearDeletedTimers(pp *p) {
	timers := pp.timers
	......
	// 对无用的切片元素赋值 nil
	for i := to; i < len(timers); i++ {
		timers[i] = nil
	}

	atomic.Xadd(&pp.deletedTimers, -cdel)
	atomic.Xadd(&pp.numTimers, -cdel)
	atomic.Xadd(&pp.adjustTimers, -cearlier)

	timers = timers[:to]
	pp.timers = timers
	updateTimer0When(pp)

	......
}

变量 to 指新切片的长度, len(timers)指原来切片的长度。

这里在其进行 timers = timers[:to] 操作前,先是将 to 数组索引后的值进行了赋值 nil。按照我们平常的用法,是没有必要执行这一步的,那为什么这里要加这一步呢?主要还是与GC 有关。

在日常开发中很少注意到这个细节,虽然最终实现的结果是一样的,但如果考虑GC的话,差别可就很大了。

假如这里不进行赋值 nil 的话,结果是没有问题的,但我们知道slice是底层是由三部分组成的。

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

slice.array 字段是一个指向底层数组的指针,这里函数的参数 pp 是指 GPM 中的 P 结构体,pp.timers 字段类型为 timers []*timer

一个应用程序会长时间处于运行状态,当GC执行的时候,会发现 pp.timers 字段是slice类型,其值类型是一个指针类型,这个切片变量(数组)会一直存在于整个应用程序的生命周期。如果不先赋值 nil 的话,GC 扫描的时候,会发现这些指针元素仍处于被切片引用状态,这样就导致一直占用内存(已无用的数据);如果先赋值的话,则在GC在扫描对象时,会发现指针元素已经没有任何对象在引用,就会立即被标记并清除(三色标记清除法),回收这些指针对象的内存。

总结

在了解这种用法前,很有必要了解当前应用的执行上下文及环境,如果只是一个临时变量且生命周期很短的话,就没有必要这样做了,如一个函数的栈变量。这里的环境P是长期处于运行状态中的指针变量,所以很有必要进行手动赋值为nil, 等待对象被GC回收。