目录
一、简介
二、类似内存泄露
1.子字符串截取
2.子切片的截取
3.指针切片截取
4.defer 导致的内存泄露
三、真内存泄露
1.协程泄露
2.time.Ticker
3.runtime.SetFinalizer
go虽然自带GC,会自动将分配在堆上且不被引用的内存回收,但如果编程中操作不当,仍然会出现 类似内存泄露或真内存泄露的情况。
此时s0与s1共享相同的底层数组,继续使用s0, 不会导致s1的其余部分内存被释放, 那么现在只有50字节内存可用,1048576-50个字节的垃圾内存不可用!
var s0 string // a package-level variable // A demo purpose function. func f(s1 string) { s0 = s1[:50] // Now, s0 shares the same underlying memory block // with s1. Although s1 is not alive now, but s0 // is still alive, so the memory block they share // couldn't be collected, though there are only 50 // bytes used in the block and all other bytes in // the block become unavailable. } func demo() { s := createStringWithLengthOnHeap(1 << 20) // 1M bytes f(s) }
为了避免这种情况,可以使用如下方法:
func f(s1 string) { s0 = string([]byte(s1[:50])) }
这种方法会产生s1前50字节的两个副本,即两次拷贝,最后一次拷贝变成s0,第一次拷贝将不在引用会被回收。
为了避免这多余的一次拷贝,可以使用go编译器专门为这种操作做的优化:
func f(s1 string) {
s0 := (" " + s1[:50])[1:]
fmt.Println(s0)
}
但这种方式有可能在后续go编译器中不再支持!
还有一种方式就是使用strings.builder 来构建字符串,其实就是将我们所需的字节拷贝到[]byte切片中,然后做了一次unsafe.Pointer的指针类型的强制拷贝!,当然绝对不是使用string([]byte())操作。
import "strings"
func f(s1 string) {
var b strings.Builder
b.Grow(50)
b.WriteString(s1[:50])
s0 = b.String()
}
与子字符串截取很相似。截取后,小切片与大切片共享相同的底层数组,只有当小切片不再被使用时,这个底层数组才能被释放。
注:底层数组只能整体释放,整体回收,参考malloc如何回收内存的。
func g(s1 []int) {
// Assume the length of s1 is much larger than 30.
s0 = s1[len(s1)-30:]
}
那么我们使用专门用于切片拷贝的copy函数就能解决这个问题。
func g(s1 []int) {
s0 = make([]int, 30)
copy(s0, s1[len(s1)-30:])
// Now, the memory block hosting the elements
// of s1 can be collected if no other values
// are referencing the memory block.
}
func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// do something with s ...
return s[1:3:3]
}
这种情况下:当函数返回后,只要s还存活,s中的所有元素都不能被释放,即使s中的第一个元素和最后一个元素没有被使用了,也不能被释放。我们可以将不要的元素置为nil就能解决这个问题。
func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// do something with s ...
// Reset pointer values.
s[0], s[len(s)-1] = nil, nil
return s[1:3:3]
}
我们都知道,defer是将执行到压到函数的最后,并按先defer后执行的顺序执行。
如果存在以下情况,在for中存在很多defer去进行资源的释放,那么这么多资源只能在函数结束时才能得到释放。
func writeManyFiles(files []File) error {
for _, file := range files {
f, err := os.Open(file.path)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
err = f.Sync()
if err != nil {
return err
}
}
return nil
}
那么我们可是使用昵名函数将这个问题解决。每一次循环后启动一个函数,函数结束后资源就释放了,不存在以上情况!
func writeManyFiles(files []File) error {
for _, file := range files {
if err := func() error {
f, err := os.Open(file.path)
if err != nil {
return err
}
// The close method will be called at
// the end of the current loop step.
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
return f.Sync()
}(); err != nil {
return err
}
}
return nil
}
如下情况:如果exit被注释,那么执行task的那个协程就永远不能退出。
func task() {
for {
select {
//case <-exit:
// return
case <-other:
//do task
}
}
}
func main() {
go task()
}
time.After在定时器到达时,会自动内回收。然后time.Ticker 钟摆不使用时,一定要Stop,不然会造成真内存泄露。至于为什么,看Ticker源码!如下代码,当task退出后,t 就不能回收。
func task() {
t := time.NewTicker(time.Second)
//defer t.Stop()
for {
select {
case <-t.C:
//do something
case <-exit:
return
}
}
}
func main() {
go task()
}
对循环引用的对象使用runtime.SetFinalizer进行终止,如下:
func memoryLeaking() {
type T struct {
v [1<<20]int
t *T
}
var finalizer = func(t *T) {
fmt.Println("finalizer called")
}
var x, y T
// The SetFinalizer call makes x escape to heap.
runtime.SetFinalizer(&x, finalizer)
// The following line forms a cyclic reference
// group with two members, x and y.
// This causes x and y are not collectable.
x.t, y.t = &y, &x // y also escapes to heap.
}
那么x、y不能被GC回收,且附着在对象上的finalizer函数将不会被调用。
标准库文档说:
runtime - The Go Programming Language
Finalizers are run in dependency order: if A points at B, both have finalizers, and they are otherwise unreachable, only the finalizer for A runs; once A is freed, the finalizer for B can run. If a cyclic structure includes a block with a finalizer, that cycle is not guaranteed to be garbage collected and the finalizer is not guaranteed to run, because there is no ordering that respects the dependencies.
总之,没事别使用runtime.SetFinalizer函数来释放对象。使用runtime.SetFinalizer前,还是参考标准库文档说明比较好!
参考文档:
Memory Leaking Scenarios -Go 101