[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問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM