go 是自带gc的语言,会自动管理内存,不用像C/C++那样,须要程序员手动释放内存,不用手动管理内存,就能少掉不少头发linux
go的GC会自动管理内存,可是这不表明go程序就不会内存泄露了。 go常见产生内存泄露的缘由就是goroutine
没有结束,简单说就是goroutine 被阻塞了,这样就会致使goroutine引用的内存不被GC回收,也就致使了内存写了。 固然产生内存泄露的缘由还有别的,只是暂时我尚未遇到。无论什么缘由产生的内存泄露,最终都是由于异常的引用,致使该被回收的内存没有被gc 回收掉程序员
提及go内存泄露分析,还要从年前的一次程序压测提及。我用一个测试程序压测咱们游戏的一些数据,大约开了3000个tcp链接到游戏。游戏数据没有问题,可是当测试结束后,发现游戏的Gateway内存占用一直没有降低。本能的让我想起了是否是内存泄露了。立刻用pprof分析了一下内存,发现果真是内存泄露了。golang
由于时公司代码,不方便拿出了分析,大致说一下缘由吧web
Gateway是一个读写分离的tcp服务,也就是每个链接都要有两个goroutine,一个读,一个写。windows
可是当tcp链接断开时,由于时序问题,致使goroutine阻塞了,一直没有结束,就是致使了相关联的内存没有释放。浏览器
由于时公司代码,(入职签了保密协议,虽然也是一个屎山,可是不能随便贴出来,避免律师函警告),此次就本身写个简单的demo模拟一下吧markdown
由于go 自带的pprof 只能展现问文字,不太明显,全部先安装一个可视化插件 graphviz 传送门
linux上能够直接经过 apt
或者 yum
安装就好了。
windows上去网站下一个就行了,我下载 .msi
格式的安装后不能用,从新下了一个 压缩包,解压后把 bin
目录配置到环境变量的 path
中就可使用了app
写一个简单的demo 模拟一下内存泄露tcp
package main
import (
"math/rand"
"net/http"
_ "net/http/pprof"
"sync"
)
func main() {
go func() {
http.ListenAndServe("0.0.0.0:8090", nil)
}()
c := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1)
for i := 0; i < 10000; i++ {
go one(c)
}
wg.Wait()
}
func one(c chan struct{}) {
var a []int64
for i := 0; i < 10000; i++ {
a = append(a, rand.Int63())
}
<-c
}
复制代码
程序很简单,在 for 循环中开启 1000 个协程,每一个协程中往切片中append 1000个数据。
用一个channel模拟协程阻塞,这样就会致使goroutine不会结束。函数
代码中监听了 8090 端口,在浏览器中输入 http://127.0.0.1:8090/debug/pprof/
下面都有解释,就不用详细介绍了,挑一两个说一下
能够看到,当前的程序总共有10005个协程,21次堆内存分配,说明咱们的协程是被阻塞的。
在命令行中,在Windows中可使用 powerShell
输入 go tool pprof -http :8081 http://127.0.0.1:8090/debug/pprof/heap
会在浏览器中打开 这就是当前 heap 采样图
能够在 VIEW 中切换不一样的显示方式
图中方块越大即是占用的内存越多,方块中连线越粗表示耗时越多
我在排查内存泄露时,当我把goroutine阻塞解决后,经过linux 的 top 命令查看Gateway内存占用时,发现内存没有降下来,一时让我陷入困惑,为啥goroutine 都结束了,为啥内存还不释放呢?直到我在网上找到了这篇文章 传送门
这位大神写的golang 相关的文章,是目前我在网上找到的最牛逼的之一,文章不光有深度,并且通俗易懂。
修改上面的代码
package main
import (
"math/rand"
"net/http"
_ "net/http/pprof"
"sync"
"time"
)
func main() {
go func() {
http.ListenAndServe("0.0.0.0:8090", nil)
}()
var wg sync.WaitGroup
wg.Add(1)
for i := 0; i < 10000; i++ {
go one()
}
wg.Wait()
}
func one() {
var a []int64
for i := 0; i < 10000; i++ {
a = append(a, rand.Int63())
}
time.Sleep(time.Second * 1)
}
复制代码
如今 go 出来的的函数 one
再也不一直阻塞,而是只会阻塞5秒
把程序运行起来看一下
等一段时间后,发现goroutine数量已经下来了,说明阻塞的协程都已经结束了,如图
可是经过任务管理器中,看到程序仍是占用着大量的内存
这时点击 heap 查看具体的堆内存状况,拉到最低下,看到一堆参数
参数不少,可是重点关注被框出来的那几个就行了,详细的分析在大神的文章中有分析,在go的库文件中也能找到,里面有详细的注释。
路径 src->runtime->mstats.go
文件中有 MemStats 的定义和注释
这些参数的单位都是字节
回到咱们的测试程序中,当全部的goroutine都结束时,GC会开始回收切片,可是被回收的内存不会直接换给操做系统,而是由go的runtime暂时保管(也就是 HeapIdle 值会变大),接下来若是再次须要分配空间,go的runtime能够不向操做系统申请内存,直接从本身保管的闲置内存中分配,这样能够提升程序性能。至于GO的runtime何时把这部份内存还给操做系统,不一样的分配策略和不一样的系统不太同样,这个有点深,我尚未深刻研究这些。不过上面传送门 的文中有介绍 MADV_FREE 有兴趣能够本身学习一下
因为水平有限,文中若有谬处,还请在评论区不吝赐教