在使用支持自动垃圾收集的语言进行编程时,通常不需要关心内存泄漏问题,因为运行时将定期收集未使用的内存。但是,我们确实需要了解一些可能导致内存泄漏的特殊场景。下文将列出几个这样的场景。

 

由子字符串(substrings)引起的内存泄漏

Go规范没有指定子字符串表达式中涉及的结果字符串和基字符串是否应该共享相同的底层内存块来承载这两个字符串的底层字节序列。标准的Go编译器/运行时允许它们共享相同的底层内存块。这是一个很好的设计,在内存和CPU消耗方面都是明智的。但是它有时会导致内存泄漏。

 

例如,在调用下面示例中的演示函数之后,将会有大约1M字节的内存泄漏,直到在其他地方再次修改包级别的变量s0。

var s0 string// package-level变量
func f(s string){
s0 = s[:50]
// s0与s共享相同的底层内存块。
// 虽然s现在不是活动的,但是s0仍然是活动的
// 所以无法收集它们共享的内存块,虽然这个块中只使用了50个字节
// 并且这个块中的所有其他字节都不可用。
}
func demo(){
s :=createStringWithLengthOnHeap(1<<20)// 1M bytes
f(s)
}

为了避免这种类型的内存泄漏,可以将子字符串转换为[]字节值,然后将[]字节值转换回字符串。

func f(s string){
s0 =string([]byte(s[:50]))

}

以上方法避免内存泄漏的缺点是在转换过程中会出现两个50字节的重复,其中一个是不必要的。可以使用标准Go编译器的优化措施,以避免不必要的重复,只需要花费一个字节内存的额外开销。

func f(s1 string){
s0 =(" "+ s1[:50])[1:]
}

上述方法的缺点是,编译器优化可能会在稍后的版本中失效,优化仅在某些编译器中有效。避免这类内存泄漏的第三种方法是使用strings.Buidler。自Go1.10之后开始支持Builder类型。

import "strings"
func f(s1 string){
var b strings.Builder
b.Grow(50)
b.WriteString(s1[:50])
s0 = b.String()

}

第三种方法的缺点是有点冗长(与前两种方法相比)。好消息是Go1.12(将于2019年初发布)开始,可以在strings标准包中调用Repeat函数来克隆字符串。从1.12开始,strings.Repeat的底层实现将使用strings.Builder生成器,以避免不必要的重复。

 

由切片引起的内存泄漏

与子字符串类似,子切片也可能导致内存泄漏。在下面的代码中,调用g函数之后,保存s1元素的内存块所占用的大部分内存将丢失(如果没有更多的值引用内存块)。

var s0 []int
func g(s1 []int){
// 假设s1的长度远大于30
s0 = s1[len(s1)-30:]
}

如果我们想避免这种类型的内存泄漏,我们必须为s0复制30个元素,这样存活的s0就不会妨碍存储s1元素的内存块被收集。

func g(s1 []int){
s0 =append([]int(nil), s1[len(s1)-30:]...)
// 现在如果没有其他值引用内存块,那么可以收集包含s1元素的内存块。

}

 

由于没有在丢失的切片元素中重置指针而导致的内存泄漏

在下面代码中,在调用h函数之后,为切片s的第一和最后一个元素分配的内存块将丢失。

func h()[]*int{
s :=[]*int{new(int),new(int),new(int),new(int)}
// 对s进行操作...
return s[1:3]
}

只要返回的切片仍然是活动的,它将阻止运行时回收s的任何元素,从而阻止为s的第一个和最后一个元素引用的两个int值分配的两个内存块被收集。

如果想避免这种类型的内存泄漏,必须重置存储在丢失元素中的指针。

func h()[]*int{
s :=[]*int{new(int),new(int),new(int),new(int)}
// do something with s ...
s[0], s[len(s)-1]=nil,nil// reset pointer values
return s[1:3]
}

注:在切片元素删除操作中,经常需要重置一些旧片元素的指针。

 

挂起Goroutines导致的内存泄漏

有时Go程序中的一些goroutine可能永远处于阻塞状态。这样的goroutines叫做挂起的goroutine。Go运行时不会杀死挂起的goroutines,因此分配给挂起goroutines的资源(以及所引用的内存块)永远不会被垃圾回收。

Go运行时不会杀死挂起的goroutines有两个原因。其一是Go运行时有时很难判断阻塞goroutine是否会永远被阻塞。另一种是,有时故意把它挂起来。例如,有时可能会让Go程序的主goroutine挂起,以避免程序退出。

我们应该避免在代码设计中出现一些由逻辑错误引起的错误。

 

time.Ticker导致的内存泄漏

当一个time.Timer值不再使用,一段时间后将被垃圾回收。但对于time.Ticker值并非如此。应该直接停止它当其不再使用的时候。

 

不正确地使用终结器(Finalizers)导致的内存泄漏

为循环引用组成员的值设置终结器可能会妨碍收集引用组内分配的所有内存块。这是真正的内存泄漏。例如,在调用并退出以下函数后,分配给x和y的内存块不能保证在以后的垃圾收集中被垃圾回收。

func memoryLeaking(){
type T struct{
v [1<<20]int
t *T
}
var finalizer =func(t *T){
fmt.Println("finalizer called")
}
var x, y T
// 调用SetFinalizer将使x逃逸到堆
runtime.SetFinalizer(&x, finalizer)
// 以下形成一个循环引用
// 这导致x和y不可收集
x.t, y.t =&y,&x // y也逃逸到堆中
}

因此避免为循环引用组中的值设置终结器。顺便说一下,不应该使用终结器作为对象析构函数。

 

原文作者:go101 译者:江玮

版权声明:本文版权归作者所有。