目录

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

go语言内存泄漏

子字符串导致的内存泄漏

使用自动垃圾回收的语言进行编程时,通常我们无需担心内存泄漏的问题,因为运行时会定期回收未使用的内存。但是如果你以为这样就完事大吉了,哪里就大错特措了。

因为,虽然go中并未对字符串时候共享底层内存块进行规定,但go语言编译器/运行时默认情况下允许字符串共享底层内存块,直到原先的字符串指向的内存被修改才会进行写时复制,这是一个很好的设计,既能节省内存,又能节省CPU资源,但有时也会导致"内存泄漏"。

例如如下代码,一旦调用demo就会导致将近1M内存的泄漏,因为s0只使用了50字节,但是会导致1M的内存一直无法被回收,这些内存会一直持续到下次s0被修改的时候才会被释放掉。

bash 复制代码
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字节的内存重复

bash 复制代码
func f(s1 string) {
	s0 = string([]byte(s1[:50]))
}

当然我们也可以利用go编译器的优化来避免不必要的重复,只需要浪费一个字节内存就行

bash 复制代码
func f(s1 string) {
	s0 = (" " + s1[:50])[1:]
}

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

避免此类内存泄漏的第三种方法是利用 Go 1.10 以来支持的 strings.Builder

bash 复制代码
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 元素的内存块所占用的大部分内存将会丢失(如果没有其他值引用该内存块)。

bash 复制代码
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 元素的内存块。

bash 复制代码
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 的第一个和最后一个元素分配的内存块将丢失。

bash 复制代码
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 值分配的两个内存块。

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

bash 复制代码
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)可能会阻止为该循环引用组分配的所有内存块被回收。这是真正的内存泄漏,不是某种假象。

例如,在调用并退出以下函数后,分配给 xy 的内存块不能保证在未来的垃圾收集中被收集。

bash 复制代码
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.
}

因此,请避免为循环引用组中的值设置终结器。

延迟函数调用导致的某种资源泄漏

非常大的延迟调用堆栈也可能会消耗大量内存,并且如果某些调用延迟太多,某些资源可能无法及时释放。

例如,如果在调用以下函数时需要处理许多文件,那么在函数退出之前将有大量文件处理程序无法释放。

bash 复制代码
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
}

对于这种情况,我们可以使用匿名函数来封装延迟调用,以便延迟函数调用能够更早地执行。例如,上面的函数可以重写并改进为

bash 复制代码
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
}

当然不要犯以下错误,需要有些同学将需要延时调用的函数字节省略,导致资源泄漏

bash 复制代码
_, err := os.Open(file.path)

如果是http请求,还会导致服务端挤压大量的连接无法释放

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
懒懒小徐11 分钟前
消息中间件面试题
java·开发语言·面试·消息队列
xcSpark1 小时前
Python基础入门(二)
开发语言·人工智能·python
qw9491 小时前
JVM:JVM与Java体系结构
java·开发语言·jvm
长流小哥2 小时前
Linux 深入浅出信号量:从线程到进程的同步与互斥实战指南
linux·c语言·开发语言·bash
予早2 小时前
Python 冷门魔术方法
开发语言·python
linux kernel2 小时前
Python基础语法2
开发语言·python
编程侦探2 小时前
【设计模式】适配器模式:让不兼容的接口和谐共处
开发语言·c++·设计模式·适配器模式
互联网搬砖老肖2 小时前
python成功解决AttributeError: can‘t set attribute ‘lines‘
开发语言·python
张立龙6662 小时前
有序二叉树各种操作实现(数据结构C语言多文件编写)
c语言·开发语言·数据结构
2401_845417452 小时前
C++ string类
java·开发语言·c++