前言:
前两天朋友问我channel是否会造成内存泄露?我告诉他不会,一般来说 只要没有对象引用关系,那么go gc就会给你标记清除掉。 但这小哥不信,一直说是内存泄露,看了朋友的go代码才知道是个怎么一回事。 该文章后续仍在不断的更新修改中, 请移步到原文地址 http://xiaorui.cc/?p=5450
简单说下他的问题,大家可以想下这么个场景,运行一个函数,他的逻辑是创建了大channel,扔给其他函数执行,但相关函数没有执行或者没有执行完就返回了。channel的buf里还堆积了大量数据,占用的对象是否被释放,go内存池里多余的mspan是否归还操作系统?
测试代码
// xiaorui.cc package main import ( "time" "fmt" ) func main() { engine() fmt.Println("start") time.Sleep(360 * time.Second) engine() fmt.Println("start") time.Sleep(360 * time.Second) engine() fmt.Println("start") time.Sleep(3600 * time.Second) } func engine() { run() } func run() { incr := 100000000 c := make(chan string, incr) s := "xxxxxxssssssssssssw" for index := 0; index < incr-10; index++ { c <- s } close(c) } ...
上面的测试代码只是为了演示内存是否释放, 工作中不应该出现这类逻辑,算是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
// xiaorui.cc scvg0: inuse: 2, idle: 5, sys: 7, released: 0, consumed: 7 (MB) scvg0: inuse: 0, idle: 1526, sys: 1526, released: 0, consumed: 1526 (MB)
调用 debug.FreeOSMemory 之后的表现, 明显看到他释放了1.5G的内存。
// xiaorui.cc scvg-1: 1526 MB released scvg-1: inuse: 0, idle: 1526, sys: 1526, released: 1526, consumed: 0 (MB) scvg0: inuse: 2, idle: 5, sys: 7, released: 0, consumed: 7 (MB) scvg0: inuse: 0, idle: 1526, sys: 1526, released: 1526, consumed: 0 (MB)
下面的 free 日志是我手动信号调用 runtime.debug.FreeOSMemory() 打印的。
debug.FreeOSMemory 做了什么?
我们先看下sysmon()监控方法。在我们启动go服务的时候,有一个线程是用来专门跑sysmon()的。 sysmon不仅可以用来抢占P,而且可以做强制runtime.GC 和 scavenge内存方式逻辑。 下面是runtime/proc.go的代码,清楚的说明2分钟为强制GC,5分钟调用scavenge释放内存。
// xiaorui.cc runtime/proc.go var forcegcperiod int64 = 2 * 60 * 1e9 // Always runs without a P, so write barriers are not allowed. // //go:nowritebarrierrec func sysmon() { lock(&sched.lock) sched.nmsys++ checkdead() unlock(&sched.lock) // If a heap span goes unused for 5 minutes after a garbage collection, // we hand it back to the operating system. scavengelimit := int64(5 * 60 * 1e9) … // scavenge heap once in a while if lastscavenge+scavengelimit/2 < now { mheap_.scavenge(int32(nscavenge), uint64(now), uint64(scavengelimit)) lastscavenge = now nscavenge++ } ...
debug.FreeOSMemory的源码,发现他会先调用一次GC,然后调用mheap的scavenge方法。
// xiaorui.cc runtime/mheap.go //go:linkname runtime_debug_freeOSMemory runtime/debug.freeOSMemory func runtime_debug_freeOSMemory() { GC() systemstack(func() { mheap_.scavenge(-1, ^uint64(0), 0) }) }
不管是sysmon和手动FreeOSMemory都调用mheap_.scavenge() ,为啥手动freeOSMemory就好用? 很明显他们之间的不同在于参数。 sysmon的释放有些严谨,freeOSMemory直接一串-1,0,0。 看起来是个最大值。
// xiaorui.cc func (h *mheap) scavenge(k int32, now, limit uint64) { // Disallow malloc or panic while holding the heap lock. We do // this here because this is an non-mallocgc entry-point to // the mheap API. ... var sumreleased uintptr for i := 0; i < len(h.free); i++ { sumreleased += scavengelist(&h.free[i], now, limit) } sumreleased += scavengetreap(h.freelarge.treap, now, limit) ... } }
总结:
在golang runtime里面不释放的内存后面是可以复用的,没必要纠结非要释放干净。还是那句话,有问题看源码。