go语言内存泄漏
子字符串导致的内存泄漏
使用自动垃圾回收的语言进行编程时,通常我们无需担心内存泄漏的问题,因为运行时会定期回收未使用的内存。但是如果你以为这样就完事大吉了,哪里就大错特措了。
因为,虽然go中并未对字符串时候共享底层内存块进行规定,但go语言编译器/运行时默认情况下允许字符串共享底层内存块,直到原先的字符串指向的内存被修改才会进行写时复制,这是一个很好的设计,既能节省内存,又能节省CPU资源,但有时也会导致"内存泄漏"。
例如如下代码,一旦调用demo就会导致将近1M内存的泄漏,因为s0只使用了50字节,但是会导致1M的内存一直无法被回收,这些内存会一直持续到下次s0被修改的时候才会被释放掉。
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)
}
为了避免这种内存泄漏,我们可以使用[]byte来替代原先的1M大小的内存,不过这样会有两次50字节的内存重复
func f(s1 string) {
s0 = string([]byte(s1[:50]))
}
当然我们也可以利用go编译器的优化来避免不必要的重复,只需要浪费一个字节内存就行
func f(s1 string) {
s0 = (" " + s1[:50])[1:]
}
上述方法的缺点是编译器优化以后可能会失效,并且其他编译器可能无法提供该优化
避免此类内存泄漏的第三种方法是利用 Go 1.10 以来支持的 strings.Builder
。
import "strings"
func f(s1 string) {
var b strings.Builder
b.Grow(50)
b.WriteString(s1[:50])
s0 = b.String()
}
从 Go 1.18 开始, strings
标准库包中新增了 Clone
函数,这成为了完成这项工作的最佳方式。
子切片导致的内存泄漏
同样场景下,切片也会导致内存的浪费
与子字符串类似,子切片也可能导致某种内存泄漏。在下面的代码中,调用 g
函数后,保存 s1
元素的内存块所占用的大部分内存将会丢失(如果没有其他值引用该内存块)。
var s0 []int
func g(s1 []int) {
// Assume the length of s1 is much larger than 30.
s0 = s1[len(s1)-30:]
}
如果我们想避免这种内存泄漏,我们必须复制 s0
的 30 个元素,这样 s0
的活跃性就不会阻止收集承载 s1
元素的内存块。
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.
}
未重置子切片指针导致的内存泄漏
在下面的代码中,调用 h
函数后,为切片 s
的第一个和最后一个元素分配的内存块将丢失。
func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// do something with s ...
// 返回一个从1开始,不能到索引3的新切片, 也就是 s[1], s[2]
return s[1:3:3]
}
只要返回的切片仍然有效,它就会阻止收集 s
的任何元素,从而阻止收集为 s
的第一个和最后一个元素引用的两个 int
值分配的两个内存块。
如果我们想避免这种内存泄漏,我们必须重置丢失元素中存储的指针。
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]
}
挂起Goroutine导致的内存泄漏
有时,Go 程序中的某些 goroutine 可能会永远处于阻塞状态。这样的 goroutine 被称为挂起的 goroutine。Go 运行时不会终止挂起的 goroutine,因此为挂起的 goroutine 分配的资源(以及它们引用的内存块)永远不会被垃圾回收。
Go 运行时不会杀死挂起的 Goroutine 有两个原因。一是 Go 运行时有时很难判断一个阻塞的 Goroutine 是否会被永久阻塞。二是我们有时会故意让 Goroutine 挂起。例如,有时我们可能会让 Go 程序的主 Goroutine 挂起,以避免程序退出。
如果不停止time.Ticker
也会导致内存泄漏
当 time.Timer
值不再使用时,它会在一段时间后被垃圾回收。但 time.Ticker
值则不然。我们应该在 time.Ticker
值不再使用时停止它。
不正确地使用终结器会导致真正的内存泄漏
为属于循环引用组的成员值设置终结器(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.
}
因此,请避免为循环引用组中的值设置终结器。
延迟函数调用导致的某种资源泄漏
非常大的延迟调用堆栈也可能会消耗大量内存,并且如果某些调用延迟太多,某些资源可能无法及时释放。
例如,如果在调用以下函数时需要处理许多文件,那么在函数退出之前将有大量文件处理程序无法释放。
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
}
当然不要犯以下错误,需要有些同学将需要延时调用的函数字节省略,导致资源泄漏
_, err := os.Open(file.path)
如果是http请求,还会导致服务端挤压大量的连接无法释放