• [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问题

    (2) 论golang Timer Reset方法使用的正确姿势  (这篇介绍非常详细)

     (3) Golang <-time.After() is not garbage collected before expiry

  • 相关阅读:
    ServiceStack支持跨域提交
    CookiesHelper
    poj 3669 线段树成段更新+区间合并
    poj2528 线段树+离散化
    hdu3308 线段树 区间合并
    hdu1542矩阵的并 线段树+扫描线
    hdu1255 矩阵的交 线段树+扫描线
    简单单点更新线段树
    树状数组模版
    hdu1873优先队列
  • 原文地址:https://www.cnblogs.com/luoming1224/p/11174927.html
Copyright © 2020-2023  润新知