無意中看到一篇文章說,當在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 }
結果如下圖所示:
從圖中可以看到該程序的內存不會再一直增長