go 内存泄漏_go 内存泄露

	- [2、goroutine阻塞拥挤等待,浪费内存](#2goroutine_151)

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、大数组作为参数导致短期内内存激增
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值