内存泄漏场景
再推一次老貘的《Go101》。
子字符串造成的内存泄露
var s0 string // 一个包级变量 // 一个演示目的函数。 func f(s1 string) { s0 = s1[:50] // 目前,s0和s1共享着承载它们的字节序列的同一个内存块。 // 虽然s1到这里已经不再被使用了,但是s0仍然在使用中, // 所以它们共享的内存块将不会被回收。虽然此内存块中 // 只有50字节被真正使用,而其它字节却无法再被使用。 } func demo() { s := createStringWithLengthOnHeap(1 << 20) // 1M bytes f(s) }
其实我们可以先将s1这个内存块转移一下,这样gc就会自动标记清理。
func f(s1 string) { s0 = string([]byte(s1[:50])) }
这个方法的缺点就是多了两次“复制”
func f(s1 string) { s0 = (" " + s1[:50])[1:] }
这个就是利用编译器的优化了,具体可以看下ssa,涉及编译器优化,就不要作为首选了。
import "strings" func f(s1 string) { var b strings.Builder b.Grow(50) b.WriteString(s1[:50]) s0 = b.String() }
这是一个看起来复杂,但却是最合理的思路。
上面讲了最合理的思路,但并不是最合理的方式,因为这种代码有坏的味道,现在golang提供了repeat支持,这是首选。
s0 = strings.Repeat(s1, 50)
可见实现思路还是一样的
func Repeat(s string, count int) string { if count == 0 { return "" } // Since we cannot return an error on overflow, // we should panic if the repeat will generate // an overflow. // See Issue golang.org/issue/16237 if count < 0 { panic("strings: negative Repeat count") } else if len(s)*count/count != len(s) { panic("strings: Repeat count causes overflow") } n := len(s) * count var b Builder b.Grow(n) b.WriteString(s) for b.Len() < n { if b.Len() <= n/2 { b.WriteString(b.String()) } else { b.WriteString(b.String()[:n-b.Len()]) break } } return b.String() }
子切片造成的内存泄露
需要了解切片的源码
var s0 []int func g(s1 []int) { // 假设s1的长度远大于30。 s0 = s1[len(s1)-30:] }
这里我们可以用append来处理,这样就不会公用一个底层了
func g(s1 []int) { s0 = append(s1[:0:0], s1[len(s1)-30:]...) }
因为未重置丢失的切片元素中的指针而造成的临时性内存泄露
func h() []*int { s := []*int{new(int), new(int), new(int), new(int)} // 使用此s切片 ... return s[1:3:3] }
其实就是一个删除元素,但是由于是指针类型,我们需要置nil的
func h() []*int { s := []*int{new(int), new(int), new(int), new(int)} s[0], s[len(s)-1] = nil, nil // 重置首尾元素指针 return s[1:3:3] }
因为协程被永久阻塞而造成的永久性内存泄露
这个Dave Cheney说过,尽量少用协程,信道,为什么?你真的需要吗?如果你写的不能百分百掌控,这些都是埋雷,Golang新手偏爱协程,但是协程我个人认为你需要把调度和运行,channel和goroutine源码看懂再写,否则是有很大风险的。
因为没有停止不再使用的time.Ticker
值而造成的永久性内存泄露
当一个time.Timer
值不再被使用,一段时间后它将被自动垃圾回收掉。 但对于一个不再使用的time.Ticker
值,我们必须调用它的Stop
方法结束它,否则它将永远不会得到回收。
因为不正确地使用终结器(finalizer)而造成的永久性内存泄露
我坚决反对新手使用这个。
延迟调用函数导致的临时性内存泄露
for里使用defer
我认为sync.pool的源码是要学习的,否则你只是在背代码。
https://github.com/golang/go/pull/32138
https://www.zhihu.com/question/327580797
https://xargin.com/logic-of-slice-memory-leak/
https://github.com/golang/go/issues/20138
https://qcrao.com/2020/05/06/dive-into-go-sync-map/