go 内存泄漏

文章目录

  • go 内存泄漏
    • 一、什么是内存泄漏
    • 二、goroutine泄漏
      • 1、什么是 goroutine 泄漏?
      • 2、goroutine 泄漏怎么导致内存泄漏?
      • 3、goroutine 泄漏的发现和定位
      • 4、goroutine 泄漏的场景
    • 三、内存泄漏的分类
      • 1、暂时性内存泄漏
      • 2、永久性内存泄漏
    • 三、其他不正当使用内存场景
      • 1、大数组作为参数导致短期内内存激增
      • 2、goroutine阻塞拥挤等待,浪费内存

go 内存泄漏

一、什么是内存泄漏

  内存泄漏是指在程序执行过程中,已不再使用的内存空间没有被及时释放或者释放时出现了错误,导致这些内存无法被使用,直到程序结束这些内存才被释放。

  如果出现内存泄漏问题,程序将会因为占据大量内存而变得异常缓慢,严重时可能会导致程序崩溃。在go语言中,可以通过runtime包里的freeosmemory()函数来进行内存回收和清理。此外,也可以使用一些工具来检测内存泄漏问题,例如go tool pprof等。

  需要注意的是,内存泄漏不是语言本身的问题,而通常是程序编写者忘记释放内存或者处理内存时出现错误导致的。

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

二、goroutine泄漏

  关于Go的内存泄漏有这么一句话:10次内存泄漏,有9次是goroutine泄漏。为避免goroutine泄漏造成内存泄漏,启动goroutine前要思考清楚:goroutine如何退出?是否会有阻塞造成无法退出?如果有,那么这个路径是否会创建大量的goroutine?

1、什么是 goroutine 泄漏?

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

2、goroutine 泄漏怎么导致内存泄漏?

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

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

  goroutine泄漏有2种方式造成内存泄漏:goroutine本身的栈所占用的空间造成内存泄漏;goroutine中的变量所占用的堆内存导致堆内存泄漏,这一部分是能通过heap profile体现出来的。

3、goroutine 泄漏的发现和定位

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

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

4、goroutine 泄漏的场景

  泄漏的场景有很多,但因channel阻塞导致泄漏的场景是最多的。

  • channel 阻塞:
    无缓冲 channel 阻塞。写操作因为没有读操作而阻塞。
    有缓冲 channel 阻塞。缓冲区满了,写操作阻塞。
    期待从channel读数据,结果没有goroutine写。

  • select 操作
    select里也是channel操作,如果所有case上的操作阻塞,且没有default分支进行处理,goroutine也无法继续执行。

  • 互斥锁没有释放,互斥锁死锁
    多个协程由于竞争资源或者彼此通信而造成阻塞,不能退出。

  • 申请过多的goroutine来不及释放

三、内存泄漏的分类

  在Go中内存泄漏分为暂时性内存泄漏和永久性内存泄漏。

1、暂时性内存泄漏

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

  • 获取长字符串中的一段导致长字符串未释放
  • 获取长slice中的一段导致长slice未释放

s0 = s1[:50],
s0 = s1[len(s1)-30:]
获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏。
字符串——两次拷贝,s0 = string([]byte(s1[:50])),s0 := (" " + s1[:50])[1:]
切片——使用copy函数,copy(s0, s1[len(s1)-30:])

  • 获取指针切片slice中的一段

s := []*int{new(int), new(int), new(int), new(int)}
return s[1:3:3]
这种情况下:当函数返回后,只要s还存活,s中的所有元素都不能被释放,即使s中的第一个元素和最后一个元素没有被使用了,也不能被释放。我们可以将不要的元素置为nil就能解决这个问题。
s := []*int{new(int), new(int), new(int), new(int)}
s[0], s[len(s)-1] = nil, nil
return s[1:3:3]

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

2、永久性内存泄漏

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

  • goroutine 泄漏导致内存泄漏;

channel 阻塞导致 goroutine 阻塞
select 操作导致 goroutine 阻塞
互斥锁没有释放,互斥锁死锁
申请过多的goroutine来不及释放

  • 定时器使用不当,time.Ticker未关闭导致内存泄漏;

time.After在定时器到达时,会自动内回收。time.Ticker 钟摆不使用时,一定要Stop,不然会造成内存泄漏。

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

三、其他不正当使用内存场景

1、大数组作为参数导致短期内内存激增

  由于数组是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的)如果是在高并发场景下每个协程都同时调用该函数,内存占用量是非常恐怖的。

  因此对于大数组放在形参场景下,通常使用切片或者指针进行传递,避免短时间的内存使用激增。

2、goroutine阻塞拥挤等待,浪费内存

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

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