go内存泄露的几个场景

目录

一、简介

二、类似内存泄露

1.子字符串截取

2.子切片的截取

3.指针切片截取

4.defer 导致的内存泄露

三、真内存泄露

1.协程泄露

2.time.Ticker

3.runtime.SetFinalizer


一、简介

        go虽然自带GC,会自动将分配在堆上且不被引用的内存回收,但如果编程中操作不当,仍然会出现 类似内存泄露真内存泄露的情况。

二、类似内存泄露

1.子字符串截取

        此时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()
}

2.子切片的截取

        与子字符串截取很相似。截取后,小切片与大切片共享相同的底层数组,只有当小切片不再被使用时,这个底层数组才能被释放。

        注:底层数组只能整体释放,整体回收,参考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.
}

3.指针切片截取

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]
}

4.defer 导致的内存泄露

        我们都知道,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
}

三、真内存泄露

1.协程泄露

        如下情况:如果exit被注释,那么执行task的那个协程就永远不能退出。


func task() {
	for {
		select {
		//case <-exit:
		//	return
		case <-other:
			//do task
		}
	}
}

func main() {
	go task()
}

2.time.Ticker

        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()
}

3.runtime.SetFinalizer

        对循环引用的对象使用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

你可能感兴趣的:(golang,golang)