不管使用什么语言,内存泄露是经常遇到的一类问题,然而使用Go语言编写内存泄露的代码却不容易,本文将列举几个可能出现内存泄露的场景,从反例中学习如何避免内存泄露
This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.
资源泄露
不关闭打开的文件
当你不再需要一个打开的文件,正常你需要调用它的Close方法,如果你不调用Close,就有可能文件描述符达到最大限制,无法在打开新的文件或者连接,程序会报too many open files
的错误。比如下面的例子:
Code 1: 文件没关闭导致 耗尽文件描述符。
|
|
在我的Mac 电脑上一个进程能打开的文件句柄数量最大是61440,也可以手动设置这个数值。go 程序会默认打开 stderr
,stdout
,stdin
三个文件句柄,一个进程最多能够打开61437 文件,多了就会报错。
http.Response.Body.Close()
Go 语言有一个 比较“知名”的bug,相信您一定看到过:如果我们忘记关闭 http 请求的body 的话,会导致内存泄露,比如下面的代码。
https://gist.github.com/hxzhouh/1e63ef82a1088ac378384e30651b20c9
code 2: http body 没有关闭导致内存泄露。
|
|
关于这个问题您可以参考下面的文章了解更多信息,后面如果有时间的话,我们从头梳理一下 net/http
-
is resp.Body.Close() necessary if we don’t read anything from the body?
- https://manishrjain.com/must-close-golang-http-response
字符串/slice 导致内存泄露
虽然 Go spec
并没有说明一个字符串表达式的结果(子)字符串和原来字符串是否共享一个内存块 但编译器确实让它们共享一个内存块,而且很多标准库包的函数原型设计也默认了这一点。这是一个好的设计,它不仅节省内存,而且还减少了CPU消耗。 但是有时候它会造成暂时性的内存泄露。
Code 3: 字符串导致内存泄露。
https://gist.github.com/hxzhouh/e09587195e2d7aa2d5f6676777c6cb16
![[Pasted image 20240925174837.png]]
为防止createStringWithLengthOnHeap
临时性内存泄露,我们可以使用strings.Clone()
Code 4 : 使用strings.Clone()
避免临时内存泄露。
|
|
这样就不会导致临时性内存泄露了。
goroutine leak
goroutine handler
绝大部分内存泄露的原因是因为goroutine泄露,比如下面的例子,很快将会内存耗尽 而导致OOM
Code 5: goroutine handler
|
|
channel
channel 的使用错误也很容易导致 goroutine
泄露,
对于无缓冲的channel,必须要等到生产者和消费者全部就绪后,才能往channel写数据,否则将会阻塞。下面的例子因为Example
提前退出导致协程泄露。
Code 6: 不合理使用无缓冲channel 导致goroutine
泄露
|
|
只需要改成有缓冲的channel 就能解决这个问题 c:= make(chan error,1)
还有一个典型的例子就是channel range 误用。
Code 7: 不合理使用range
导致goroutine
泄露
|
|
channel 可以使用 range
迭代 .但是一旦读取不到内容,range 就会等待 channel 的写入,而 range 如果正好在 goroutine 内部,这个 goroutine 就会被阻塞,造成泄露。正确的做法是: 在wg.Wait()
后面 close channel.
runtime.SetFinalizer误用
如果两个对象都设置了 runtime.SetFinalizer
并且他们之间存在 “循环引⽤” ,那么这两个对象将会泄露,即时他们不再使用,GC
也不会回收他们。
关于 runtime.SetFinalizer
的更多内容,可以参考我的另外一篇文章
time.Ticker
这是go 1.23 版本之前的问题了, 如果我们不调用ticker.Stop()
.go 1.23 已经不会造成泄露了 https://go.dev/doc/go1.23#timer-changes
defer
我们一般习惯在defer
中释放资源 defer
函数本身不会导致内存泄露。但是它的两个机制可能会导致内存临时性泄露。
- 执行时间,
defer
总是在函数结束的运行。如果您的函数运行时间过长,或者永远不会结束,那么您在defer 中释放的资源可能,很久都不会被释放,或者永远都不被释放。 defer
本身也需要占用内存,每个defer
都会在内存中添加一个调用点。如果您在循环中使用defer,有可能会导致临时性的内存泄露。
Code 8:defer
导致 内存临时泄露
|
|
比如上面的代码,不仅仅可能会导致 defer
临时内存泄露,还可能会导致too many open files
不要痴迷于使用defer
除非你觉得代码你的代码可能会panic
,否则及时关闭文件是一个更好的选择。
总结
本文列举了几种可能会导致go 内存泄露的行为,同时 Goroutine 内存泄漏是 Go 语言最容易发生的内存泄漏情况,它通常伴随着错误地使用 goroutine 和 channel等。而 channel 的特殊用法如 select 和 range 又让 channel 阻塞变得更加隐蔽不易发现,进而增加排查内存泄漏的难度。
遇到内存泄露问题,我们可以通过 pprof 帮助我们快速的定位问题,希望我们每个人都能写出健壮的代码。