Published on

go内存泄漏的几种可能

Authors
  • avatar
    Name
    yushenw
    Linkedin

长期存在的goroutine引用对象

func leakyFunction(){
  data := make([]int, 1000)
  go func() {
    // 这个goroutine不会退出,因此它引用的data永远不会被回收
    for {
      time.Sleep(time.Hour)
      _ = data[0] // 保持对data的引用
    }
  }
}

goroutine中添加退出机制,避免goroutine不退出造成的长时间引用

func nonLeakyFunction() {
    data := make([]int, 1000)
    go func() {
        // 添加退出机制
        for {
            select {
            case <-time.After(time.Hour):
                _ = data[0]
            case <-exitChan:
                return
            }
        }
    }()
}

全局变量引起的内存占用

以下示例globalData申请了一个1000个整数的内存空间,知道程序结束,会造成程序占用, 当需要申请大量内存时,最好局部(按需)声明变量引用。

var globalData = make([]int, 1000)

func doSomething() {
    // 操作全局变量
    globalData[0] = 1
}

使用局部变量,按需申请内存

func doSomething() {
    localData := make([]int, 1000)
    // 使用局部变量代替全局变量
    localData[0] = 1
}

未关闭的channel

未关闭的channel会导致潜在的内存占用,以下例子没有关闭内部的ch,channel内部的数据可能得不到释放, 另外接收方的goroutine因为发送没有关闭channel而可能会永久的等待channel数据,这回造成相关资源得不到回收。

func leakyChan(){
  ch := make(chan int)
  go func(){
    for i := 0; i < 10; i++ {
      ch <- i
    }
    // 没有关闭channel
    // close(ch)
  }()
}

循环引用

type Node struct {
  data String
  next *Node
}

func createCycle(){
  node1 := &Node{data: "node1"}
  node2 := &Nod{data: "node2"}
  node1.next = node2
  node2.next = node1
}

Go的垃圾收集器能处理大多数循环引用,但在某些复杂的情况下,循环引用可能导致内存泄漏。 所以要注意设计清晰的数据结构,避免不必要的循环引用。

未关闭的context

func leakyContext() {
    ctx, _ := context.WithCancel(context.Background())
    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                return
            default:
                // 执行一些操作
            }
        }
    }(ctx)
    // 忘记调用cancel函数
}

忘记调用由context.WithCancel(context.Background())创建的cancel函数会导致一些潜在的资源无法得到释放。这些资源主要包括与context对象关联的资源和可能被阻塞的goroutine。

  • Context的内部结构: 每个通过context.WithCancel创建的context对象内部都有一些结构,用于处理取消操作和传递取消信号。这些结构包括用于通知取消的channel和与此相关的数据。如果cancel函数没有被调用,这些结构可能不会被及时清理。

  • 阻塞的goroutine goroutine在ctx.Done()上等待取消信号。如果取消函数(cancel)没有被调用,这个goroutine将永远等待,因此不会退出。这意味着goroutine占用的所有资源(如栈内存)都不会被释放。

所以在不再需要context时,应该调用其对应的cancel函数。这是良好的编程实践,可以确保资源得到正确释放。 也可以使用defer保证调用:在可能的情况下,可以在创建context的同一作用域中使用defer来调用cancel,以确保即使在发生错误或提前返回的情况下也能保证调用cancel。

在这个例子中,合理的做法是在创建context后立即使用defer来安排调用其cancel函数,或者在不再需要context时显式调用cancel。这样可以确保相关资源被及时清理,防止潜在的资源泄漏。

defer语句在运行时间较长的函数中

func leakyDefer() {
    f, _ := os.Open("file.txt")
    // 应该直接调用f.Close() 及时关闭文件
    defer f.Close() // 延迟关闭文件,文件在函数运行期间一直打开

    // 长时间执行的操作
    time.Sleep(time.Hour)
}

在Go中,由于其自动垃圾回收机制,内存通常在没有任何有效引用时被自动回收。但是,即使在Go中,良好的内存管理仍然是重要的,特别是在处理大型数据结构或在并发环境中。例如,长时间运行的goroutine如果保持对大型数据结构的引用,即使这些数据结构实际上已不再需要,也可能导致内存占用不必要地高。 由于编程错误,一块内存仍被错误地引用(即使它实际上已不再需要),垃圾回收器就不会回收这块内存。这就是内存泄漏的典型情况,其中内存实际上对程序的其余部分是无用的,但由于错误地保留了对它的引用,它无法被回收。

为了避免这些问题,建议进行仔细的代码审查,使用工具(如pprof)进行性能分析和内存分析,以及编写有效的测试来监测和防止内存泄漏的发生。

其它

有些语言没有自动GC,开发人员可能需要手动回收内存,否则内存不可访问和管理后会造成内存泄漏,比如c

//定义两个指针分别指向两块内存
char * p1 = (char *)malloc(10);
char * p2 = (char *)malloc(10);
//将p1指向p2指向的内存
p1 = p2

此举会造成p1原来指向的内存泄口

除了c语言,还有c++、Rust、Fortran、Pascal和汇编语言没有真正意义上的自动GC