最近项目开发中遇到一个问题:在程序中大量使用golang slice导致内存占用暴涨。经过一番分析与定位最终解决了问题,把过程记录下来与大家分析。
1. 问题现象
- 程序正常运行中突然出现内存占用飙升,使用htop命令查看,程序内存占用排第一;
- 使用free -m命令查看,发现可用内存为0;
- 由于程序占用内存过多,导致linux虚拟机响应缓慢卡顿;
- 使用kill命令杀掉进程后,内存释放,虚拟机恢复正常。
2. 查看内存占用情况
因为是内存问题,所以首先使用golang的pprof包进行查看heap和alloc内存分配详情。
2.1 引入pprof包
//在import中添加pprof包
_ "net/http/pprof"
2.2 打开pprof包web服务端口
//指定pprof对外提供的http服务的ip和端口,配置为0.0.0.0表示可以非本机访问
go func() {
http.ListenAndServe("0.0.0.0:9999", nil)
}()
2.3 程序启动后查看pprof服务端口是否启用
netstat -antp|grep 9999
tcp6 0 0 :::9989 :::* LISTEN 28294/program
2.4 在网页上查看pprof信息
http://127.0.0.1:9999/debug/pprof/
2.5 更丰富的命令和信息,还是要通过命令行查看
go tool pprof http://127.0.0.1:9999/debug/pprof/heap
连接上后,可以使用top命令查看内存使用排行
然后,使用list命令直接可以查看到具体是哪一行分配了多少内存
3. 分析问题
由于pprof信息显示,大量内存都是由于在slice的append方法中分配的。于是又回顾了一下slice的原理和坑。
3.1 slice原理
golang中slice是对数组的引用,底层实现实际上还是数组。对slice一定要谨慎使用append操作。如果cap未变化时,slice是对数组的引用,并且append会修改被引用数组的值。append操作导致cap变化后,会复制被引用的数组,然后切断引用关系。
3.2 golang关于slice的内存回收
经过分析和查资料发现,网上总结的容易导致内存不能及时回收的情况:
3.2.1截取长slice中的一段导致长slice未释放。
由于底层都是数组,如果截图长slice的一段,其实相当于引用了底层数组中的一小段。只要还有引用,golang的gc就不能回收数组。这种情况导致未使用的数组空间,未及时回收。
解决方案,新建一个长度为0的slice,将需要的一小段slice使用append方法添加到新的slice。再将原来的slice置为nil。
3.2.2 配合gc,及时将不再使用的slice置为nil
如果slice中包含很多元素,再只有一小部分元素需要使用的情况下。建议重新分配一个slice将需要保留的元素加入其中,将原来的长slice整个置为nil。