开发者

go语言内存泄漏的常见形式

目录
  • go语言内存泄漏
  • 子字符串导致的内存泄漏
  • 子切片导致的内存泄漏
  • 未重置子切片指针导致的内存泄漏
  • 挂起Goroutine导致的内存泄漏
    • 如果不停止time.Ticker也会导致内存泄漏
    • 不正确地使用终结器会导致真正的内存泄漏
    • 延迟函数调用导致的某种资源泄漏

go语言内存泄漏

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

上述方法的缺python点是编译器优化以后可能会失效,并且其他编译器可能无法提供该优化

避免此类内存泄漏的第三种方法是利用 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 函数,这成为了完成这项工作的最android佳方式。

子切片导致的内存泄漏

同样场景下,切片也会导致内存的浪费

与子字符串类似,子切片也可能导致某种内存泄漏。在下面的代码中,调用 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 元素的内存块js

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

只要返回的zfRwOertTH切片仍然有效,它就会阻止收集 s 的任何元素,从而阻止收集为 s 的第一个和最后一个元素引用的两个 int 值分配的两个内存块。

如果我们想避免这种内存泄漏,我们必须重置丢失元素中存储的指针。

func h() []*int {
	s := []*int{new(int), new(int), new(int), new(int)}
	// do sometphphing 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请求,还会导致服务端挤压大量的连接无法释放

到此这篇关于go语言内存泄漏的常见形式的文章就介绍到这了,更多相关go语言内存泄漏内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新开发

开发排行榜