无意中看到一篇文章说,当在for循环里使用select + time.After的组合时会产生内存泄露,于是进行了复现和验证,以此记录

内存泄露复现

问题复现测试代码如下所示:

 

package main

import (
    "time"
    )

func main()  {
    ch := make(chan int, 10)

    go func() {
        var i = 1
        for {
            i++
            ch <- i
        }
    }()

    for {
        select {
        case x := <- ch:
            println(x)
        case <- time.After(3 * time.Minute):
            println(time.Now().Unix())
        }
    }
}

执行go run test_time.go,通过top命令,我们可以看到该小程序的内存一直飙升,一小会就能占用3G多内存,如下图:

Go语言pprof 泄露_Go语言pprof 泄露

原因分析

 在for循环每次select的时候,都会实例化一个一个新的定时器。该定时器在3分钟后,才会被激活,但是激活后已经跟select无引用关系,被gc给清理掉。
换句话说,被遗弃的time.After定时任务还是在时间堆里面,定时任务未到期之前,是不会被gc清理的。

也就是说每次循环实例化的新定时器对象需要3分钟才会可能被GC清理掉,如果我们把上面复现代码中的3分钟改小点,改成10秒钟,通过top命令会发现大概10秒钟后,该程序占用的内存增长到1.05G后基本上就不增长了

原理验证

通过runtime.MemStats可以看到程序中产生的对象数量,我们可以验证一下上面的原理

验证代码如下所示:

package main

import (
    "time"
    "runtime"
    "fmt"
    )

func main()  {
    var ms runtime.MemStats
    runtime.ReadMemStats(&ms)
    fmt.Println("before, have", runtime.NumGoroutine(), "goroutines,", ms.Alloc, "bytes allocated", ms.HeapObjects, "heap object")
    for i := 0; i < 1000000; i++ {
        time.After(3 * time.Minute)
    }
    runtime.GC()
    runtime.ReadMemStats(&ms)
    fmt.Println("after, have", runtime.NumGoroutine(), "goroutines,", ms.Alloc, "bytes allocated", ms.HeapObjects, "heap object")

    time.Sleep(10 * time.Second)
    runtime.GC()
    runtime.ReadMemStats(&ms)
    fmt.Println("after 10sec, have", runtime.NumGoroutine(), "goroutines,", ms.Alloc, "bytes allocated", ms.HeapObjects, "heap object")

    time.Sleep(3 * time.Minute)
    runtime.GC()
    runtime.ReadMemStats(&ms)
    fmt.Println("after 3min, have", runtime.NumGoroutine(), "goroutines,", ms.Alloc, "bytes allocated", ms.HeapObjects, "heap object")
}

验证结果如下图所示:

Go语言pprof 泄露_定时任务_02

从图中可以看出,实例中循环跑完后,创建了3000152个对象,由于每个time定时器设置的为3分钟,在3分钟后,可以看到对象都被GC回收,只剩153个对象,从而验证了,time.After定时器在定时任务到达之前,会一直存在于时间堆中,不会释放资源,直到定时任务时间到达后才会释放资源。

问题解决

综上,在go代码中,在for循环里不要使用select + time.After的组合,可以使用time.NewTimer替代

示例代码如下所示:

package main

import (
    "time"
    )

func main()  {
    ch := make(chan int, 10)

    go func() {
        for {
            ch <- 100
        }
    }()

    idleDuration := 3 * time.Minute
    idleDelay := time.NewTimer(idleDuration)
    defer idleDelay.Stop()

    for {
        idleDelay.Reset(idleDuration)

        select {
            case x := <- ch:
                println(x)
            case <-idleDelay.C:
                return
            }
    }
}

 

结果如下图所示:

Go语言pprof 泄露_for循环_03

从图中可以看到该程序的内存不会再一直增长

参考文章

(1) 分析golang time.After引起内存暴增OOM问题