[golang]golang time.After記憶體洩露問題分析
阿新 • • 發佈:2019-07-12
無意中看到一篇文章說,當在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