Go 語言中手動記憶體管理
Go 目前的 GC 實現比較簡單(mark-sweep演算法), 程序的記憶體使用量取決於兩次GC操作直接的記憶體申請量(不能重複使用), 而且通常GC發生在函式呼叫的深處, 大量物件無法立即釋放. 另外, 目前Go對記憶體的使用是貪婪的, 一旦向系統申請了就不再釋放, 進一步增大了記憶體消耗(但不是洩露). 整體看來, 對某些有大量臨時記憶體的應用, 記憶體消耗量可能會是同樣功能的C程式10倍, 甚至更多.
Beansdb 的 Proxy 是用 Go 實現的, 其中一個部署圖片和歌曲的例項也面臨了這個問題, 執行一段時間後記憶體的使用量會增長到3-4G (與訪問量相關), 另一個儲存小物件的例項則穩定在100M以內. Proxy 的每次請求, 都要申請一個平均 100k (10k - 3M) 的buffer用來臨時儲存資料, 它佔了整個記憶體消耗的絕大部分, 如果能夠手動管理這些buffer的使用, 應該能夠大大降低記憶體消耗.
runtime 模組有 Alloc() 和 Free(), 能夠申請後釋放記憶體, 通過refect模組做型別轉換後能夠給buffer使用. 但是它申請和釋放的記憶體也是有GC統一管理的, 一旦申請就不再還給系統. 因此我們需要把系統的malloc() 和 free() 直接封裝了給Go呼叫, 通過 CGO 可以簡單實現, 如下:
package cmem
//include <stdlib.h>
import "C"
import "unsafe"
func Alloc(size uintptr) *byte {
return (*byte)(C.malloc(_Ctypedef_size_t(size)))
}
func Free(ptr *byte) {
C.free(unsafe.Pointer(ptr))
}
在需要使用手動分配記憶體的地方:
//item.Body = make([]byte, length)
item.alloc = cmem.Alloc(uintptr(length))
item.Body = (*[1 << 30]byte)(unsafe.Pointer(item.alloc))[:length]
(*reflect.SliceHeader)(unsafe.Pointer(&item.Body)).Cap = length
一旦臨時物件使用完畢, 可以立即釋放記憶體:
if item.alloc != nil {
cmem.Free(item.alloc)
item.alloc = nil
}
另外, 為了防止記憶體洩露(某些情況下漏了主動是否記憶體), 可以使用runtime的Finalize機制來釋放記憶體:
runtime.SetFinalizer(item, func(item *Item) {
if item.alloc != nil {
cmem.Free(item.alloc)
item.alloc = nil
}
})
通過這種簡單策略, 可以大大減少這種大的臨時物件對記憶體的消耗, Proxy 在連續執行幾天後記憶體也穩定在 200-300M 左右, 即使短時間內記憶體消耗上升, 之後如果訪問壓力下降, 記憶體使用量也會降下來.