无意中看到一篇文章说,当在for循环里使用select + time.After的组合时会产生内存泄露,于是进行了复现和验证,以此记录
内存泄露复现
问题复现测试代码如下所示:
1 package main 2 3 import ( 4 \"time\" 5 ) 6 7 func main() { 8 ch := make(chan int, 10) 9 10 go func() { 11 var i = 1 12 for { 13 i++ 14 ch <- i 15 } 16 }() 17 18 for { 19 select { 20 case x := <- ch: 21 println(x) 22 case <- time.After(3 * time.Minute): 23 println(time.Now().Unix()) 24 } 25 } 26 }
执行go run test_time.go,通过top命令,我们可以看到该小程序的内存一直飙升,一小会就能占用3G多内存,如下图:
原因分析
在for循环每次select的时候,都会实例化一个一个新的定时器。该定时器在3分钟后,才会被激活,但是激活后已经跟select无引用关系,被gc给清理掉。
换句话说,被遗弃的time.After定时任务还是在时间堆里面,定时任务未到期之前,是不会被gc清理的。
也就是说每次循环实例化的新定时器对象需要3分钟才会可能被GC清理掉,如果我们把上面复现代码中的3分钟改小点,改成10秒钟,通过top命令会发现大概10秒钟后,该程序占用的内存增长到1.05G后基本上就不增长了
原理验证
通过runtime.MemStats可以看到程序中产生的对象数量,我们可以验证一下上面的原理
验证代码如下所示:
1 package main 2 3 import ( 4 \"time\" 5 \"runtime\" 6 \"fmt\" 7 ) 8 9 func main() { 10 var ms runtime.MemStats 11 runtime.ReadMemStats(&ms) 12 fmt.Println(\"before, have\", runtime.NumGoroutine(), \"goroutines,\", ms.Alloc, \"bytes allocated\", ms.HeapObjects, \"heap object\") 13 for i := 0; i < 1000000; i++ { 14 time.After(3 * time.Minute) 15 } 16 runtime.GC() 17 runtime.ReadMemStats(&ms) 18 fmt.Println(\"after, have\", runtime.NumGoroutine(), \"goroutines,\", ms.Alloc, \"bytes allocated\", ms.HeapObjects, \"heap object\") 19 20 time.Sleep(10 * time.Second) 21 runtime.GC() 22 runtime.ReadMemStats(&ms) 23 fmt.Println(\"after 10sec, have\", runtime.NumGoroutine(), \"goroutines,\", ms.Alloc, \"bytes allocated\", ms.HeapObjects, \"heap object\") 24 25 time.Sleep(3 * time.Minute) 26 runtime.GC() 27 runtime.ReadMemStats(&ms) 28 fmt.Println(\"after 3min, have\", runtime.NumGoroutine(), \"goroutines,\", ms.Alloc, \"bytes allocated\", ms.HeapObjects, \"heap object\") 29 }
验证结果如下图所示:
从图中可以看出,实例中循环跑完后,创建了3000152个对象,由于每个time定时器设置的为3分钟,在3分钟后,可以看到对象都被GC回收,只剩153个对象,从而验证了,time.After定时器在定时任务到达之前,会一直存在于时间堆中,不会释放资源,直到定时任务时间到达后才会释放资源。
问题解决
综上,在go代码中,在for循环里不要使用select + time.After的组合,可以使用time.NewTimer替代
示例代码如下所示:
1 package main 2 3 import ( 4 \"time\" 5 ) 6 7 func main() { 8 ch := make(chan int, 10) 9 10 go func() { 11 for { 12 ch <- 100 13 } 14 }() 15 16 idleDuration := 3 * time.Minute 17 idleDelay := time.NewTimer(idleDuration) 18 defer idleDelay.Stop() 19 20 for { 21 idleDelay.Reset(idleDuration) 22 23 select { 24 case x := <- ch: 25 println(x) 26 case <-idleDelay.C: 27 return 28 } 29 } 30 }
结果如下图所示:
从图中可以看到该程序的内存不会再一直增长