慎用time.After会造成内存泄漏(go) 本是古典 何须时尚 2022-12-08 15:47 210阅读 0赞 ## 2020-09-24更新 ## 修复文章的问题: * 去除使用`time.Ticker`方法修复bug,不符合select超时逻辑 * 以前使用go tool pprof分析内存占用方法是错误的,现在已经更改过来了。 ## 前言 ## > 嗨,大家好,我是asong,我今天又来了。昨天发表了一篇文章:[手把手教姐姐写消息队列][Link 1],其中一段代码被细心的读者发现了有内存泄漏的危险,确实是这样,自己没有注意到这方面,追求完美的我,马上进行了排查并更改了这个`bug`。现在我就把这个`bug`分享一下,避免小伙伴们后续踩坑。 > 测试代码已经放到了github:https://github.com/asong2020/Golang\_Dream/tree/master/code\_demo/time\_oom\_validate > > 欢迎star~~~ ## 背景 ## 我先贴一下会发生内存泄漏的代码段,根据代码可以更好的进行讲解: func (b *BrokerImpl) broadcast(msg interface{ }, subscribers []chan interface{ }) { count := len(subscribers) concurrency := 1 switch { case count > 1000: concurrency = 3 case count > 100: concurrency = 2 default: concurrency = 1 } pub := func(start int) { for j := start; j < count; j += concurrency { select { case subscribers[j] <- msg: case <-time.After(time.Millisecond * 5): case <-b.exit: return } } } for i := 0; i < concurrency; i++ { go pub(i) } } 看了这段代码,你知道是哪里发生内存泄漏了嘛?我先来告诉大家,这里`time.After(time.Millisecond * 5)`会发生内存泄漏,具体原因嘛别着急,我们一步步分析。 ## 验证 ## 我们来写一段代码进行验证,先看代码吧: package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) /** time.After oom 验证demo */ func main() { ch := make(chan string,100) go func() { for { ch <- "asong" } }() go func() { // 开启pprof,监听请求 ip := "127.0.0.1:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } }() for { select { case <-ch: case <- time.After(time.Minute * 3): } } } 这段代码我们该怎么验证呢?看代码估计你们也猜到了,没错就是`go tool pprof`,可能有些小伙伴不知道这个工具,那我简单介绍一下基本使用,不做详细介绍,更多功能可自行学习。 再介绍`pprof`之前,我们其实还有一种方法,可以测试此段代码是否发生了内存泄漏,就是使用`top`命令查看该进程占用`cpu`情况,输入`top`命令,我们会看到`cpu`一直在飙升,这种方法可以确定发生内存泄漏,但是不能确定发生问题的代码在哪部分,所以最好还是使用`pprof`工具进行分析,他可以确定具体出现问题的代码。 ### proof 介绍 ### 定位goroutine泄露会使用到pprof,pprof是Go的性能工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。使用pprof有多种方式,Go已经现成封装好了1个:`net/http/pprof`,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务,能够通过浏览器和命令行2种方式获取运行数据。 基本使用也很简单,看这段代码: package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { // 开启pprof,监听请求 ip := "127.0.0.1:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } } 使用还是很简单的吧,这样我们就开启了`go tool pprof`。下面我们开始实践来说明`pprof`的使用。 ### 验证流程 ### 首先我们先运行我的测试代码,然后打开我们的终端输入如下命令: $ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap 浏览器会自动弹出,看下图: ![ae8b8b0ff1fd6a10015aafeafef62753.png][] 看这个图,都爆红了,`time.Timer`导致占用CPU内存飙升,现在找到问题了,下面我们就可以来分析一下了。 ## 原因分析 ## 分析具体原因之前,我们先来了解一下go中两个定时器`ticker`和`timer`,因为不知道这两个的使用,确实不知道具体原因。 ### ticker和timer ### Golang中time包有两个定时器,分别为ticker 和 timer。两者都可以实现定时功能,但各自都有自己的使用场景。 我们来看一下他们的区别: * ticker定时器表示每隔一段时间就执行一次,一般可执行多次。 * timer定时器表示在一段时间后执行,默认情况下只执行一次,如果想再次执行的话,每次都需要调用 time.Reset()方法,此时效果类似ticker定时器。同时也可以调用stop()方法取消定时器 * timer定时器比ticker定时器多一个Reset()方法,两者都有Stop()方法,表示停止定时器,底层都调用了stopTimer()函数。 ### 原因 ### 上面我们了介绍go的两个定时器,现在我们回到我们的问题,我们的代码使用time.After来做超时控制,`time.After`其实内部调用的就是`timer`定时器,根据`timer`定时器的特点,具体原因就很明显了。 这里我们的定时时间设置的是3分钟, 在for循环每次select的时候,都会实例化一个一个新的定时器。该定时器在3分钟后,才会被激活,但是激活后已经跟select无引用关系,被gc给清理掉。这里最关键的一点是**在计时器触发之前,垃圾收集器不会回收 Timer**,换句话说,被遗弃的time.After定时任务还是在时间堆里面,定时任务未到期之前,是不会被gc清理的,所以这就是会造成内存泄漏的原因。每次循环实例化的新定时器对象需要3分钟才会可能被GC清理掉,如果我们把上面代码中的3分钟改小点,会有所改善,但是仍存在风险,下面我们就使用正确的方法来修复这个bug。 ## 修复bug ## ### 使用`timer`定时器 ### `time.After`虽然调用的是`timer`定时器,但是他没有使用`time.Reset()` 方法再次激活定时器,所以每一次都是新创建的实例,才会造成的内存泄漏,我们添加上`time.Reset`每次重新激活定时器,即可完成解决问题。 func (b *BrokerImpl) broadcast(msg interface{ }, subscribers []chan interface{ }) { count := len(subscribers) concurrency := 1 switch { case count > 1000: concurrency = 3 case count > 100: concurrency = 2 default: concurrency = 1 } //采用Timer 而不是使用time.After 原因:time.After会产生内存泄漏 在计时器触发之前,垃圾回收器不会回收Timer pub := func(start int) { idleDuration := 5 * time.Millisecond idleTimeout := time.NewTimer(idleDuration) defer idleTimeout.Stop() for j := start; j < count; j += concurrency { if !idleTimeout.Stop(){ select { case <- idleTimeout.C: default: } } idleTimeout.Reset(idleDuration) select { case subscribers[j] <- msg: case <-idleTimeout.C: case <-b.exit: return } } } for i := 0; i < concurrency; i++ { go pub(i) } } ## 总结 ## > 不知道这篇文章你们看懂了吗?没看懂的可以下载测试代码,自己测试一下,更能加深印象的呦~~~ > > 这篇文章主要介绍了排查问题的思路,`go tool pprof`这个工具很重要,遇到性能和内存gc问题,都可以使用golang tool pprof来排查分析问题。不会的小伙伴还是要学起来的呀~~~ > > 最后感谢指出问题的那位网友,让我又有所收获,非常感谢,所以说嘛,还是要共同进步的呀,你不会的,并不代表别人不会,虚心使人进步嘛,加油各位小伙伴们~~~ **结尾给大家发一个小福利吧,最近我在看\[微服务架构设计模式\]这一本书,讲的很好,自己也收集了一本PDF,有需要的小伙可以到自行下载。获取方式:关注公众号:\[Golang梦工厂\],后台回复:\[微服务\],即可获取。** **我翻译了一份GIN中文文档,会定期进行维护,有需要的小伙伴后台回复\[gin\]即可下载。** **我是asong,一名普普通通的程序猿,让我一起慢慢变强吧。我自己建了一个`golang`交流群,有需要的小伙伴加我`vx`,我拉你入群。欢迎各位的关注,我们下期见~~~** ![74db1636cd7db570336d086fc026ae41.png][] 推荐往期文章: * [手把手教姐姐写消息队列][Link 1] * [详解Context包,看这一篇就够了!!!][Context] * [go-ElasticSearch入门看这一篇就够了(一)][go-ElasticSearch] * [面试官:go中for-range使用过吗?这几个问题你能解释一下原因吗][go_for-range] * [学会wire依赖注入、cron定时任务其实就这么简单!][wire_cron] * [听说你还不会jwt和swagger-饭我都不吃了带着实践项目我就来了][jwt_swagger-] * [掌握这些Go语言特性,你的水平将提高N个档次(二)][Go_N] * [go实现多人聊天室,在这里你想聊什么都可以的啦!!!][go] * [grpc实践-学会grpc就是这么简单][grpc_-_grpc] * [go标准库rpc实践][go_rpc] * [2020最新Gin框架中文文档 asong又捡起来了英语,用心翻译][2020_Gin_ asong] * [基于gin的几种热加载方式][gin] * [boss: 这小子还不会使用validator库进行数据校验,开了~~~][boss_ _validator] [Link 1]: https://mp.weixin.qq.com/s/0MykGst1e2pgnXXUjojvhQ [ae8b8b0ff1fd6a10015aafeafef62753.png]: /images/20221123/55a44cc2db8d42baab849dfe07853b95.png [74db1636cd7db570336d086fc026ae41.png]: /images/20221123/a1c16802ece34b6ca4507afc257761c9.png [Context]: https://mp.weixin.qq.com/s/JKMHUpwXzLoSzWt_ElptFg [go-ElasticSearch]: https://mp.weixin.qq.com/s/mV2hnfctQuRLRKpPPT9XRw [go_for-range]: https://mp.weixin.qq.com/s/G7z80u83LTgLyfHgzgrd9g [wire_cron]: https://mp.weixin.qq.com/s/qmbCmwZGmqKIZDlNs_a3Vw [jwt_swagger-]: https://mp.weixin.qq.com/s/z-PGZE84STccvfkf8ehTgA [Go_N]: https://mp.weixin.qq.com/s/7yyo83SzgQbEB7QWGY7k-w [go]: https://mp.weixin.qq.com/s/H7F85CncQNdnPsjvGiemtg [grpc_-_grpc]: https://mp.weixin.qq.com/s/mOkihZEO7uwEAnnRKGdkLA [go_rpc]: https://mp.weixin.qq.com/s/d0xKVe_Cq1WsUGZxIlU8mw [2020_Gin_ asong]: https://mp.weixin.qq.com/s/vx8A6EEO2mgEMteUZNzkDg [gin]: https://mp.weixin.qq.com/s/CZvjXp3dimU-2hZlvsLfsw [boss_ _validator]: https://mp.weixin.qq.com/s?__biz=MzIzMDU0MTA3Nw==&mid=2247483829&idx=1&sn=d7cf4f46ea038a68e74a4bf00bbf64a9&scene=19&token=1606435091&lang=zh_CN#wechat_redirect
相关 内存泄漏问题:Java对象何时会成为内存泄漏的源头? 在Java编程中,内存泄漏通常是指程序无法释放不再使用的内存空间。以下是一些可能导致Java对象成为内存泄漏源头的情况: 1. 引用循环:当一个对象被多个线程共享,并且它们之 布满荆棘的人生/ 2024年09月10日 08:45/ 0 赞/ 43 阅读
相关 ThreadLocal搭配线程池时为什么会造成内存泄漏? 在Java中,ThreadLocal是一个用于在多线程环境下存储线程局部变量的工具类。它允许每个线程都拥有自己独立的变量副本,这样每个线程可以独立地操作自己的变量副本,而不会影 一时失言乱红尘/ 2023年10月13日 16:34/ 0 赞/ 37 阅读
相关 慎用time.After会造成内存泄漏(go) 2020-09-24更新 修复文章的问题: 去除使用`time.Ticker`方法修复bug,不符合select超时逻辑 以前使用go tool pprof 本是古典 何须时尚/ 2022年12月08日 15:47/ 0 赞/ 211 阅读
相关 Go语言内存泄漏问题排查总结 文章目录 背景 环境准备 业务中内存泄漏的现象以及排查思路 内存泄漏的现象 排查思路 内存泄漏的拓展思考 G 红太狼/ 2022年10月10日 09:50/ 0 赞/ 527 阅读
相关 会造成内存泄漏的操作 内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。 如果一个对象的引用数量为 0(没 川长思鸟来/ 2022年07月15日 09:25/ 0 赞/ 180 阅读
相关 JS哪些操作会造成内存泄露 内存泄漏:指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。 1、JS的回收机制 JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释 女爷i/ 2022年06月09日 14:51/ 0 赞/ 192 阅读
相关 哪些操作会造成内存泄漏 内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象), 绝地灬酷狼/ 2022年06月06日 06:52/ 0 赞/ 239 阅读
相关 JS中哪些操作会造成内存泄露? 内存泄漏:指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。 1、JS的回收机制 JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其 太过爱你忘了你带给我的痛/ 2022年05月24日 11:58/ 0 赞/ 198 阅读
相关 那些操作会造成内存泄漏? 内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象) 爱被打了一巴掌/ 2022年03月18日 13:42/ 0 赞/ 252 阅读
还没有评论,来说两句吧...