[這是一道送命題] 使用字串傳值居然也不會釋放原始字串的記憶體地址!!!

DukeAnn發表於2018-12-13

這個問題陸陸續續查了一週才實錘到記憶體洩露的點

問題的開端是這樣的,以前大哥走了,我接了這個專案發現居然在記憶體洩露,每天要重啟好幾次,決定解決一下,沒想到解決了一週。

專案使用了一個包叫 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 協議》,轉載必須註明作者和本文連結

做自己

相關文章