2025-04-06    2025-04-06    1754 字  4 分钟
Go

内存泄露发生的可能情况

暂时性内存泄露

  • 获取长字符串中的一段导致长字符串未释放
  • 获取长slice中的一段导致长slice未释放
  • 在长slice新建slice导致泄漏

string相比于切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或切片中的一段内容,由于新生成的对象和老的string或切片共用一个内存空间,
会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄露。

永久性内存泄露

  • goroutine泄漏
  • time.Ticker未关闭导致泄漏
  • Finalizer导致泄漏
  • Deferring Function Call导致泄漏

以下分别说明发生的可能

数组的错误使用

由于数组是Golang的基本数据类型,每个数组占用不同的内存空间,生命周期互不干扰,很难出现内存泄漏的情况。但是数组作为形参传输时,遵循的是值拷贝,如果函数被多次调用且数组过大时,则会导致内存使用激增。

1
2
3
4
5
6
7
8
//统计nums中target出现的次数
func countTarget(nums [1000000]int, target int) int {
    num := 0
    for i := 0; i < len(nums) && nums[i] == target; i++ {
        num++
    }
    return num
}

例如上面的函数中,每次调用countTarget函数传参时都需要新建一个大小为100万的int数组,大约为8MB内存,如果在短时间内调用100次就需要约800MB的内存空间了。(未达到GC时间或者GC阀值是不会触发GC的)如果是在高并发场景下每个协程都同时调用该函数,内存占用量是非常恐怖的。

对于大数组放在形参场景下,通常使用切片或者指针进行传递,避免短时间的内存使用激增。

Goroutine引起的内存泄漏,未及时退出

实际开发中更多的还是Goroutine引起的内存泄漏,因为Goroutine的创建非常简单,通过关键字go即可创建,由于开发的进度大部分程序猿只会关心代码的功能是否实现,很少会关心Goroutine何时退出。如果Goroutine在执行时被阻塞而无法退出,就会导致Goroutine的内存泄漏,一个Goroutine的最低栈大小为2KB,在高并发的场景下,对内存的消耗也是非常恐怖的!

互斥锁未释放

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 1//协程拿到锁未释放,其他协程获取锁会阻塞
 2func mutexTest() {
 3    mutex := sync.Mutex{}
 4    for i := 0; i < 10; i++ {
 5        go func() {
 6            mutex.Lock()
 7            fmt.Printf("%d goroutine get mutex", i)
 8      //模拟实际开发中的操作耗时
 9            time.Sleep(100 * time.Millisecond)
10        }()
11    }
12    time.Sleep(10 * time.Second)
13}

死锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 1func mutexTest() {
 2    m1, m2 := sync.Mutex{}, sync.RWMutex{}
 3  //g1得到锁1去获取锁2
 4    go func() {
 5        m1.Lock()
 6        fmt.Println("g1 get m1")
 7        time.Sleep(1 * time.Second)
 8        m2.Lock()
 9        fmt.Println("g1 get m2")
10    }()
11    //g2得到锁2去获取锁1
12    go func() {
13        m2.Lock()
14        fmt.Println("g2 get m2")
15        time.Sleep(1 * time.Second)
16        m1.Lock()
17        fmt.Println("g2 get m1")
18    }()
19  //其余协程获取锁都会失败
20    go func() {
21        m1.Lock()
22        fmt.Println("g3 get m1")
23    }()
24    time.Sleep(10 * time.Second)
25}

空channel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 1func channelTest() {
 2  //声明未初始化的channel读写都会阻塞
 3    var c chan int
 4  //向channel中写数据
 5    go func() {
 6        c <- 1
 7        fmt.Println("g1 send succeed")
 8        time.Sleep(1 * time.Second)
 9    }()
10  //从channel中读数据
11    go func() {
12        <-c
13        fmt.Println("g2 receive succeed")
14        time.Sleep(1 * time.Second)
15    }()
16    time.Sleep(10 * time.Second)
17}

能出不能进

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 1func channelTest() {
 2    var c = make(chan int)
 3  //10个协程向channel中写数据
 4    for i := 0; i < 10; i++ {
 5        go func() {
 6            c <- 1
 7            fmt.Println("g1 send succeed")
 8            time.Sleep(1 * time.Second)
 9        }()
10    }
11  //1个协程丛channel中读数据
12    go func() {
13        <-c
14        fmt.Println("g2 receive succeed")
15        time.Sleep(1 * time.Second)
16    }()
17  //会有写的9个协程阻塞得不到释放
18    time.Sleep(10 * time.Second)
19}

能进不能出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 1func channelTest() {
 2    var c = make(chan int)
 3  //10个协程向channel中读数据
 4    for i := 0; i < 10; i++ {
 5        go func() {
 6            <- c
 7            fmt.Println("g1 receive succeed")
 8            time.Sleep(1 * time.Second)
 9        }()
10    }
11  //1个协程丛channel写读数据
12    go func() {
13        c <- 1
14        fmt.Println("g2 send succeed")
15        time.Sleep(1 * time.Second)
16    }()
17  //会有读的9个协程阻塞得不到释放
18    time.Sleep(10 * time.Second)
19}

time.Ticker未及时调用stop导致

time.Ticker是每隔指定的时间就会向通道内写数据。作为循环触发器,必须调用stop方法才会停止,从而被GC掉,否则会一直占用内存空间。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 1func tickerTest() {
 2    //定义一个ticker,每隔500毫秒触发
 3    ticker := time.NewTicker(time.Second * 1)
 4    //Ticker触发
 5    go func() {
 6        for t := range ticker.C {
 7            fmt.Println("ticker被触发", t)
 8        }
 9    }()
10
11    time.Sleep(time.Second * 10)
12    //停止ticker
13    ticker.Stop()
14}

// go垃圾回收 // go多线程,多进程怎么用,与go routing的区别 // 工作中与同事争论,怎么去解决争论