[这是一道送命题] 使用字符串传值居然也不会释放原始字符串的内存地址!!!

这个问题陆陆续续查了一周才实锤到内存泄露的点

问题的开端是这样的,以前大哥走了,我接了这个项目发现居然在内存泄露,每天要重启好几次,决定解决一下,没想到解决了一周。

项目使用了一个包叫 gjson (链接),我们有两个缓存一个叫cache1 gjson.Result{},一个叫 cache2 map[string]string 。cache2 的数据来源是一个 URL 每分钟更新一次使用 gjson 解析 接口返回的 json 数据,cache1 的数据来源是从 cache2 可以匹配上的数据通过 gjson 获取的字符串就放到 cache 1 map 中(当 cache1 把 cache2 键全匹配完了以后就不会再更新了。我也不知道这大哥怎么想的,顺手把这个 bug 也给修了)。

更新 cache2 的是一个 goroutine 60s 一次,匹配 cache1 又是一个 goroutine,所以呢 cache1 是从很多的 cache2 版本中拼凑而来的,因为 cache2 总更新。问题就来了,随着每次更新 cache2 我的内存占用就越来越大。使用 pprof 查看是在包内部出字节切片转字符串的时候开辟内存不能释放,如下

有图

我这一看这有问题啊,为什么我把一个从 gjson.Result.Get("key").String() 获取来的 string 赋值给一个 map[string]string,在 cache2 goroutine 后就不在使用这个 gjson.Result 变量了,而是创建了新的 gjson.Result ,为什么这个旧的 gjson.Result 的内存地址不能释放呢?Go里面不是字符串传值是完全拷贝吗?没道理啊?

在我研究了很久写了 N 个 demo 去排除各种情况后我开始看他的包是怎么给我的这个字符串,我看到了他给我的字符串是靠字符串位置获取的:

var cache2 string
var cache1 map[string]string    

cache2 = string([]byte) // 这个转换创建的内存空间不会被释放 []byte 相当于接口返回
cache1["key"] = cache2[0:2] // 这玩意居然是个 string 不是 slice

好了明白了。因为我 cache1 挂上了 cache2 字符串的某一小段字符串,所以整个 cache2 根本不会被释放。在随着不停的更新 cache2 ,就会越来越大,因为只要挂上一个字母他就不会释放了。妈了个卖批!!!(不好意思)字符串还有这样骚操作传值。

问题的解决方式有就是在开辟一个 []byte 把获取的字符串 append() 进去:

string(append([]byte(nil), gjson.Result.Get("name").String()...))

或者使用 strings.Builder{} 结构体,它里面的实现和上面的方法类似,不过版本低的 Go 没这个方法。

好了,出坑

本作品采用《CC 协议》,转载必须注明作者和本文链接
做自己
讨论数量: 1

你好,上面内存分析用的什么,能方便说一下

4年前 评论
hanjl (作者) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!