前言:
前两天朋友问我channel是否会造成内存泄露?我告诉他不会,一般来说 只要没有对象引用关系,那么go gc就会给你标记清除掉。 但这小哥不信,一直说是内存泄露,看了朋友的go代码才知道是个怎么一回事。
简单说下他的问题,大家可以想下这么个场景,运行一个函数,他的逻辑是创建了大channel,扔给其他函数执行,但相关函数没有执行或者没有执行完就返回了。channel的buf里还堆积了大量数据,占用的对象是否被释放,go内存池里多余的mspan是否归还操作系统?
测试代码
上面的测试代码只是为了演示内存是否释放, 工作中不应该出现这类逻辑,算是bug了。
再次明确下答案,只要你的channel没有引用关系了,就算你没有close关闭或者chan有大量的堆积数据没有消费,最终会被gc释放。 通过runtime的memstats可以看到memory heap stats各个数据的状态。
但是我们直接通过top查看该进程的内存, 还是有1.5G的空间占用。
在经过几次ForceGC和scavenge后,才会释放内存给操作系统。 尝试过多次,基本在15分钟左右。
在没有释放内存的时间窗口里,空闲的mspan没有释放回去,可能被mcache freelist拿着,可能被mcentral拿着,也就是说,没有归还给操作系统sys。
花样测试
我们可以测试一下被go内存池占用的mspan是否可以得到复用?
曾经的一个问题
上面说的channel和map,在没有引用关系的情况下,等待一段时间后内存会释放。
但是全局的channel和map会有啥体现? 我曾经测试过全局的channel在消费干净后,内存会在几次scvg之后被释放。 但是全局的大map在全部key被delete后不会释放干净,只会释放一部分内存,等了好久也没有继续释放。
我们知道go内存池为了避免频繁的malloc内存,减少系统调用,所以把内存放置到go内存池里。 但好几个大g被占用,说不过去。虽然Runtime会每隔2分钟进行强制GC,每隔5分钟调用scvg释放归还系统内存,但全局map总是释放不干净。
解决方案
我们优先应该想到的是怎么解除引用关系?
使用新对象替换全局对象或者是重置成nil,但是nil明显不合理,会造成panic。map和channel都是引用类型,没有引用关系了,自然就会被gc和scavenge。
如果不能使用替换引用的方法,可以使用 runtime提供的 debug.FreeOSMemory 方法, 文档https://golang.org/pkg/runtime/debug/#FreeOSMemory,在各类go社区里大家说这个方法危险,毕竟他前面有个debug。 但怎么就危险了,貌似没人说明白。
我自己用FreeOSMemory的两个使用技巧:
第一种,监听自定义的信号,当接收signal时,回调 debug.FreeOSMemory 。
下面是GODEBUG=gctrace=1的日志,debug.FreeOSMemory调用之前的gctrace
调用 debug.FreeOSMemory 之后的表现, 明显看到他释放了1.5G的内存。
下面的 free 日志是我手动信号调用 runtime.debug.FreeOSMemory() 打印的。
debug.FreeOSMemory 做了什么?
我们先看下sysmon()监控方法。在我们启动go服务的时候,有一个线程是用来专门跑sysmon()的。 sysmon不仅可以用来抢占P,而且可以做强制runtime.GC 和 scavenge内存方式逻辑。 下面是runtime/proc.go的代码,清楚的说明2分钟为强制GC,5分钟调用scavenge释放内存。
debug.FreeOSMemory的源码,发现他会先调用一次GC,然后调用mheap的scavenge方法。
不管是sysmon和手动FreeOSMemory都调用mheap_.scavenge() ,为啥手动freeOSMemory就好用? 很明显他们之间的不同在于参数。 sysmon的释放有些严谨,freeOSMemory直接一串-1,0,0。 看起来是个最大值。
总结:
在golang runtime里面不释放的内存后面是可以复用的,没必要纠结非要释放干净。还是那句话,有问题看源码。