Golang time.After内存泄漏分析
本篇博客的视频教程首发于 Youtube:科技小飞哥,加入 电报粉丝群 获得最新视频更新和问题解答。
背景
我刚转做go语言开发开始写入职小程序的时候,写下了如下的代码:
这只是一个简单的从连接池取连接
的代码,如果连接池没有可用连接,就在超时后创建一个新的连接使用。
说实话,我觉得我的代码可优雅了,后来对go语言有了更深入的了解之后,发现我写的代码有着明显的内存泄漏的问题。
熟悉go语言的朋友可能已经看出来了,内存泄漏的代码就是time.After
。
这是go语言经典的坑
,或者说是对go语言机制不了解的初学者经常会踩的坑,稍不留神就会掉到坑里去。
今天我们就来分析一下这个time.After
内存泄漏之谜。
我们来用最简单的例子模拟一下:
|
|
咱也别管代码优不优雅,就很简单的逻辑,先往channel里面存数据,然后不停的使用for select case
语法从channel里面取数据。
我相信大家一眼就能看懂。就是这么简单的代码却会导致内存泄漏。
那么我们使用top
指令看看:
发现我们的进程MEM
这一列的值迅速增加,不到一分钟就占用了6G的内存,明显产生了内存泄漏。
原理分析
要了解为什么会产生内存泄漏,我们需要看看time package
里面time.After
函数的的定义。https://pkg.go.dev/time
func After(d Duration) <-chan Time
After waits for the duration to elapse and then sends the current time on the returned channel. It is equivalent to NewTimer(d).C. The underlying Timer is not recovered by the garbage collector until the timer fires. If efficiency is a concern, use NewTimer instead and call Timer.Stop if the timer is no longer needed.
该方法可以在一定时间(根据所传入的 Duration)后主动返回 time.Time 类型的 channel 消息。
注意: 描述里面写的很清楚:
在计时器触发之前,垃圾收集器不会回收Timer
如果考虑效率,需要使用NewTimer替代
回过头来看我们的代码,这里我们的定时时间设置的是3分钟, 在for循环内每次select的时候,都会实例化一个一个新的定时器。该定时器在3分钟后,才会被激活,但是激活后已经跟select无引用关系,被gc给清理掉。
重点是: 在select里面虽然我们没有执行到time.After,但是这个对象已经初始化了,依然在时间堆里面,定时任务未到期之前,是不会被gc清理的。
解决方案
根据time.After
的解释,我们知道,for select case
里面最好不要用time.After
,go语言对这个方法的设计决定了它不能这么用,否则会有内存泄漏的风险。
至于解决方案,time package
的文档也说明了,使用NewTimer
来做定时器,不需要每次都创建定时器对象。
time.After
虽然调用的是timer定时器,但是他没有使用time.Reset()
方法再次激活定时器,所以每一次都是新创建的实例,才会造成的内存泄漏。
而我们使用NewTimer
创建定时器,再加上time.Reset
每次重新激活定时器,即可完美解决问题。
|
|
NewTimer creates a new Timer that will send the current time on its channel after at least duration d.
而Timer
的指针里面封装了:
The Timer type represents a single event. When the Timer expires, the current time will be sent on C, unless the Timer was created by AfterFunc. A Timer must be created with NewTimer or AfterFunc.
我们改造一下上面的Get函数,使用time.NewTimer
来初始化Timer
的指针,并用Reset
来重置定时器。
记得一定要使用Reset重置定时器,如果不重置,那么定时器还是从创建的时候开始计算时间流逝。使用了Reset之后,每次都从当前Reset的时间开始算。
这么改之后,再次测试代码,发现内存一直稳定。
总结
在for select case
里面使用定时器一定要小心,定时器只有等到计时器触发之后才会被垃圾回收器回收,而time.After
是单次触发且无法Reset。
这种需要循环内触发定时器的用例,还是要使用time.NewTimer
并手动Reset
。
<全文完>