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 左右, 即使短时间内内存消耗上升, 之后如果访问压力下降, 内存使用量也会降下来.
-------------------------------------------------------------------------------------------------------------
Go有两种分配内存的机制,规则很简单,下面来简单介绍一下。
1、new函数
New()函数可以给一个值类型的数据分配内存(不知道什么是值类型请前往切片那一部分),调用成功后返回一个初始化的内存块指针,同时该类型被初始化为0值,原型定义:
func new(Type) * Type
new是一个分配内存的内置函数,但是不同于其他语言中new所做的工作,它只是将内存清零,而不是初始化内存。
2、make函数
Make()函数用于给引用类型分配内存空间,比如:slice,map,channal等,这里需要注意的一点是make()创建的是一个引用类型的对象,而不是一个内存空间的指针。Make()函数原型:
func make(Type,size IntegerType)Type
参数Type必须是一个引用类型(slice,map,channal);参数IntegerType指定要创建的该对象的个数。和new不同的是make调用成功是返回一个对象,而不是一个内存空间的指针。
new 的作用是初始化一个指向类型的指针(*T),make 的作用是为 slice,map 或 chan 初始化并返回引用(T)
package main
import (
"fmt"
)
func main() {
p := new([]int)
fmt.Println(p) //输出&[],p本身是一个地址
s := make([]int, 1)
fmt.Println(s) //输出[0],s本身是一个slice对象,其内容默认为0
}
通过这个例子可以看出,当对slice,map以及channel进行初始化时,使用make比new方式要好,而其他形式的则利用new进行初始化