1. 程式人生 > >[golang]golang time.After記憶體洩露問題分析

[golang]golang time.After記憶體洩露問題分析

無意中看到一篇文章說,當在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 }

結果如下圖所示:

從圖中可以看到該程式的記憶體不會再一直增長

參考文章

(1) 分析golang time.After引起記憶體暴增OOM