Go 语言中手动内存管理


Go 语言是自带GC的, 相对C语言等的手动内存管理省事很多, 弊端便是会消耗更多的内存, 以及在GC时导致整个程序的停顿. 在某些特殊场合, 如果能够可选地手动进行内存管理, 效果会好不少.

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 左右, 即使短时间内内存消耗上升, 之后如果访问压力下降, 内存使用量也会降下来.