golang 内存泄漏总结

1.内存泄漏归纳

简单归纳一下,还是“临时性”内存泄露和“永久性”内存泄露:

临时性泄露,指的是该释放的内存资源没有及时释放,对应的内存资源仍然有机会在更晚些时候被释放,即便如此在内存资源紧张情况下,也会是个问题。这类主要是 string、slice 底层 buffer 的错误共享,导致无用数据对象无法及时释放,或者 defer 函数导致的资源没有及时释放。


永久性泄露,指的是在进程后续生命周期内,泄露的内存都没有机会回收,如 goroutine 内部预期之外的for-loop或者chan select-case导致的无法退出的情况,导致协程栈及引用内存永久泄露问题。

1.1.什么是内存泄露

内存泄露指的是程序运行过程中已不再使用的内存,没有被释放掉,导致这些内存无法被使用,直到程序结束这些内存才被释放的问题。

Go虽然有GC来回收不再使用的堆内存,减轻了开发人员对内存的管理负担,但这并不意味着Go程序不再有内存泄露问题。在Go程序中,如果没有Go语言的编程思维,也不遵守良好的编程实践,就可能埋下隐患,造成内存泄露问题。

关于Go的内存泄露有这么一句话:

10次内存泄露,有9次是goroutine泄露。 

这篇文章主要介绍Go程序的goroutine泄露,掌握了如何定位和解决goroutine泄露,就掌握了内存泄露的大部分场景。

1.2.goroutine泄露

1.2.1.什么是goroutine泄露

如果你启动了1个goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是goroutine泄露。

提前思考:什么会导致goroutine无法退出/阻塞?

本质:Goroutine泄露的本质是channel阻塞,无法继续向下执行,导致此goroutine关联的内存都无法释放,进一步造成内存泄露。

1.2.2.goroutine泄露怎么导致内存泄露

每个goroutine占用2KB内存,泄露1百万goroutine至少泄露2KB * 1000000 = 2GB内存,为什么说至少呢?

Goroutine执行过程中还存在一些变量,如果这些变量指向堆内存中的内存,GC会认为这些内存仍在使用,不会对其进行回收,这些内存谁都无法使用,造成了内存泄露

goroutine泄露有2种方式造成内存泄露:

  1. goroutine本身的栈所占用的空间造成内存泄露。
  2. goroutine中的变量所占用的堆内存导致堆内存泄露,这一部分是能通过heap profile体现出来的。

1.2.3.goroutine泄露的发现和定位

利用好go pprof获取goroutine profile文件,然后利用3个命令top、traces、list定位内存泄露的原因。

判断依据:在节点正常运行的情况下,隔一段时间获取goroutine的数量,如果后面获取的那次,某些goroutine比前一次多,如果多获取几次,是持续增长的,就极有可能是goroutine泄露。

1.2.4.goroutine泄露的场景

泄露的场景不仅限于以下两类,但因channel相关的泄露是最多的。

  1. channel的读或者写:
    1. 无缓冲channel的阻塞通常是写操作因为没有读而阻塞
    2. 有缓冲的channel因为缓冲区满了,写操作阻塞
    3. 期待从channel读数据,结果没有goroutine写
  1. select操作,select里也是channel操作,如果所有case上的操作阻塞,goroutine也无法继续执行。

1.2.5.编码goroutine泄露的建议

为避免goroutine泄露造成内存泄露,启动goroutine前要思考清楚:

  1. goroutine如何退出?
  2. 是否会有阻塞造成无法退出?如果有,那么这个路径是否会创建大量的goroutine?

2.Go101总结 常见内存泄漏的情况

Go程序可能会在一些情况下造成内存泄漏。go101网站总结了各种内存泄漏的情况:

2.1.获取长字符串中的一段导致长字符串未释放

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 f(s1 string) {
	s0 = (" " + s1[:50])[1:]
}

2.2. 获取长slice中的一段导致长slice未释放

var s0 []int

func g(s1 []int) {
	// Assume the length of s1 is much larger than 30.
	s0 = s1[len(s1)-30:]
}

func demo() {
	s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
	f(s)
}

解决方案:

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

2.3.长slice新建slice导致泄漏

func h() []*int {
	s := []*int{new(int), new(int), new(int), new(int)}
	// do something with s ...

	return s[1:3:3]
}

解决方案:

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

2.4.goroutine泄漏

package main

import (
	"fmt"
	_ "net/http/pprof"
	"time"
)

func main() {
	ch := make(chan int)

	for {
		time.Sleep(1 * time.Second)
		alloc(ch)
	}
}

func alloc(ch chan<- int) {
	go func() {
		fmt.Println("new goroutine")
		ch <- 0
		fmt.Println("finished goroutine")
	}()
}
输出
 GOROOT=/usr/local/go #gosetup
GOPATH=/Users/qicycle/Go #gosetup
/usr/local/go/bin/go build -o /private/var/folders/26/ynhz7g5n3xg19q_bnpqcjpdc0000gn/T/___go_build_goroutineLeak_go -gcflags all=-N -l /System/Volumes/Data/Users/qicycle/Documents/我的测试/golang/memoak/goroutineLeak.go #gosetup
/Applications/GoLand.app/Contents/plugins/go/lib/dlv/mac/dlv --listen=0.0.0.0:51575 --headless=true --api-version=2 --check-go-version=false --only-same-user=false exec /private/var/folders/26/ynhz7g5n3xg19q_bnpqcjpdc0000gn/T/___go_build_goroutineLeak_go --
API server listening at: [::]:51575
debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-1100.0.30..1
 for x86_64.
Got a connection, launched process /private/var/folders/26/ynhz7g5n3xg19q_bnpqcjpdc0000gn/T/___go_build_goroutineLeak_go (pid = 3348).
new goroutine
new goroutine
new goroutine
new goroutine
new goroutine
new goroutine
new goroutine
new goroutine
^Znew goroutine
new goroutine
new goroutine
new goroutine
new goroutine

通过输出可以看到,只有new goroutine,没有finished goroutine。

goroutine泄露导致内存泄露: channel有写没有读的情况,导致goroutine一直阻塞着无法完成形成泄漏
goroutine无法完成的情况有很多种,比如互斥锁没有释放,互斥锁死锁等等

2.5. time.Ticker未关闭导致泄漏

import (
	"fmt"
	_ "net/http/pprof"
	"time"
)

func main() {
	timer := time.NewTicker(time.Duration(2) * time.Second)
	//defer timer.Stop()
	for true {
		select {
		case <-timer.C:
			fmt.Println("on time")
		default:
			time.Sleep(1 * time.Second)
		}
	}
}

2.6. Finalizer导致泄漏

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

	// 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.
}

2.7. Deferring Function Call导致泄漏

大量的文件只有等待函数结束才释放,属于临时泄漏

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
}

3. 其他泄漏/不正当使用内存

3.1. 内存分配没有释放

以下实例内存一直申请,但是没有释放,造成内存泄漏


package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"os"
	"time"
)

// 内存堆积没有释放
func main() {
	// 开启pprof
	go func() {
		ip := "0.0.0.0:6060"
		if err := http.ListenAndServe(ip, nil); err != nil {
			fmt.Printf("start pprof failed on %s\n", ip)
			os.Exit(1)
		}
	}()

	tick := time.Tick(time.Second / 100)
	var buf []byte
	for range tick {
		buf = append(buf, make([]byte, 1024*1024)...)
	}
}

3.2. 大数组作为参数导致短期内内存激增

由于数组是Golang的基本数据类型,每个数组占用不同的内存空间,生命周期互不干扰,很难出现内存泄漏的情况。
但是数组作为形参传输时,遵循的是值拷贝,如果函数被多次调用且数组过大时,则会导致内存使用激增。

func countTarget(nums [1000000]int, target int) int{
   num := 0
   for i:=0; i<len(nums) && nums[i] == target; i++{
      num ++
   }
   return num
}

例如上面的函数中,每次调用countTarget函数传参时都需要新建一个大小为100万的int数组,大约为8MB内存,
如果在短时间内调用100次就需要约800MB的内存空间了。(未达到GC时间或者GC阀值是不会触发GC的)
如果是在高并发场景下每个协程都同时调用该函数,内存占用量是非常恐怖的。
因此对于大数组放在形参场景下,通常使用切片或者指针进行传递,避免短时间的内存使用激增。

3.3. goroutine阻塞拥挤等待,内存浪费

10个生产者1秒生产一次,同时只有1个消费者1秒消费一次,导致9个生产者都在阻塞等待,浪费内存资源

package main

import (
	"fmt"
	"time"
)

var ch = make(chan string, 1)

func producer(){
	for i:=0; i<10; i++ {
		go func(pi int) {
			n := 0
			for {
				n += 1
				ch <- fmt.Sprintf("%d_%d", pi, n)
				fmt.Printf("%s producer write p:%d, n:%d\n", time.Now().String(), pi, n)
				time.Sleep(1 * time.Second)
			}
		}(i)
	}
}

func consumer(){
	for {
		gotS := <- ch
		fmt.Printf("%s consumer read n:%s\n", time.Now().String(), gotS)
		time.Sleep(1 * time.Second)
	}
}

func main() {
	go consumer()
	go producer()
	select{

	}
}

 

4. 需要手动管理内存吗?

go语言不需要手动管理内存;go语言内置内存管理功能(GC机制),开发者不需要关心内存的申请和释放,这样为使用者带来极大的便利。

什么是GC,又有什么用?

GC,全称 Garbage Collection,即垃圾回收,是一种自动内存管理的机制。

当程序向操作系统申请的内存不再需要时,垃圾回收主动将其回收并供其他代码进行内存申请时候复用,或者将其归还给操作系统,这种针对内存级别资源的自动回收过程,即为垃圾回收。而负责垃圾回收的程序组件,即为垃圾回收器。

当然对于临时的内存泄漏是可以注意一下的。

GC将在另外一篇单独做个说明

posted @ 2023-02-07 14:27  若-飞  阅读(2283)  评论(0)    收藏  举报