分享
  1. 首页
  2. 文章

[golang]golang time.After使用不当导致内存泄露问题分析

luoming1224 · · 3423 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

无意中看到一篇文章说,当在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


有疑问加站长微信联系(非本文作者)

本文来自:博客园

感谢作者:luoming1224

查看原文:[golang]golang time.After使用不当导致内存泄露问题分析

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
3423 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏