从Golang Slice的内存泄漏来理解Slice的使用逻辑

5,394 阅读2分钟

Golang虽然是自带GC的语言,仍然存在内存泄漏的情况,这片文章总结了Golang中内存泄漏的情况

其中Slice的内存泄漏是最容易中招的,看看这个PR: writev 的 leak,Golang官方都踩了坑。

本文将就其中的Slice内存泄漏的情况做分析,并介绍Slice实现和使用的一些关键逻辑。

Slice如何内存泄漏

Golang是自带GC的,如果资源一直被占用,是不会被自动释放的,比如下面的代码,如果传入的slice b是很大的,然后引用很小部分给全局量a,那么b未被引用的部分就不会被释放,造成了所谓的内存泄漏。

var a []int

func test(b []int) {
	a = b[:1]
	return
}

想要理解这个内存泄漏,主要就是理解上面的a = b[:1]是一个引用,其实新、旧slice指向的都是同一片内存地址,那么只要全局量a在,b就不会被回收。

Slice的使用逻辑

关于新、旧slice指向同一片地址空间,具体可以看下面的代码和说明图,关键点在于

  • b:=a[1:3]时,ba指向了同一片地址上的sliceb看到的是索引为1和2的两个成员,所以长度为2, 2也指定了b的读写长度。
  • 通过修改b[0]的值为11a[1]的值也会随之改变,验证了他们指向同一个地址空间
  • b的容量为9,代表了b引用slice的真实长度
  • 可以通过b=a[1:3:2],将bcap限制为2
func main() {
	a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	b := a[1:3]
	b[0] = 11	// b[0]的改写,即对a[1]的改写
	fmt.Println(a[1]) // a[1]被写成了11
	fmt.Println(len(a), cap(a))  // 10  10
	fmt.Println(len(b), cap(b))	 // 2    9
}

如何避免问题

如果想避免这个问题,文章顶部的链接里给出了方法, 它之所以能够重新分配的原因在于append方法的实现,如果append的目标slice空间不够,会重新申请一个array来放需要append的内容,所以&b[0]&a[0]的值是不一样的,而&a[0]&c[0]地址是一致的:

var b []int
var c []int
// 现在,如果再没有其它值引用着承载着a元素的内存块,
// 则此内存块可以被回收了。
func test(a []int) {
	c = a[:1]
	b = append(a[:0:0], a[:1]...)
	
	fmt.Println(&a[0], &c[0], &b[0]) //0xc0000aa030 0xc0000aa030 0xc0000b2038
}