一些减小 gc 压力、避免内存泄漏的小技巧分享。
减少对象分配
所谓减少对象的分配,实际上是尽量做到,对象的重用。 比如像如下的两个函数定义:
1
2
3
func(r*Reader)Read()([]byte, error)
func(r*Reader)Read(buf []byte)(int, error) // ok 有形参
第一个函数没有形参,每次调用的时候返回一个 []byte
,第二个函数在每次调用的时候,形参是一个 buf []byte
类型的对象,之后返回读入的byte的数目。
第一个函数在每次调用的时候都会分配一段空间,这会给 gc 造成额外的压力。第二个函数在每次调用的时候,会重用形参声明。
避免 string
与 []byte
转化
老生常谈 string
与 []byte
转化,在 stirng
与 []byte
之间进行转换,会给 gc
造成压力 通过 gdb
,可以先对比下两者的数据结构:
1
2
type = struct []uint8 { uint8 *array; int len; int cap;}
type = struct string { uint8 *str; int len;}
两者发生转换的时候,底层数据结结构会进行复制,因此导致 gc
效率会变低。解决策略上,一种方式是一直使用 []byte
,特别是在数据传输方面,[]byte
中也包含着许多 string
会常用到的有效的操作。另一种是使用更为底层的操作直接进行转化,避免复制行为的发生,使用 unsafe.Pointer
直接进行转化。
字符串连接
尽少使用 +
连接 string 由于采用 +
来进行 string 的连接会生成新的对象,降低 gc
的效率,好的方式是通过 append
函数来进行。
但是还有一个弊端,比如参考如下代码:
1
2
3
b := make([]int, 1024)
b = append(b, 99)
fmt.Println("len:", len(b), "cap:", cap(b))
指定 slice 长度
在使用了 append
操作之后,数组的空间由 1024 增长到了 1312 ,所以如果能提前知道数组的长度的话,最好在最初分配空间的时候就做好空间规划操作,会增加一些代码管理的成本,同时也会降低 gc 的压力,提升代码的效率。
对上面的代码可以这样改进:
1
b := make([]int, 0, 1024)
内存泄漏大部分原因是代码里新开的协程有引用地方,导致GC无法释放。
避免内存泄漏的两个原则
1、 绝对不能由消费者关 channel,因为向关闭的 channel 写数据会 panic。正确的姿势是生产者写完所有数据后,关闭 channel,消费者负责消费完 channel 里面的全部数据:
1
2
3
4
5
6
7
8
9
10
11
func produce(ch chan<- T) {
defer close(ch) // 生产者写完数据关闭 channel
ch <- T{}
}
func consume(ch <-chan T) {
for _ = range ch { // 消费者用for-range读完里面所有数据
}
}
ch := make(chan T)
go produce(ch)
consume(ch)
为什么 consume
要读完 channel
里面所有数据?因为 go produce()
可能有多个,这样写的代码,在读完ch可以确定所有produce的goroutine都退出了,不会泄漏。
2、 利用关闭 channel 来广播取消动作。向关闭的 channel 读数据永远不会阻塞,这是进阶的技巧。假设消费者拿到数据处理后有 error 发生,整个动作失败,那么需要有某种机制通知生产者停止并退出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func produce(ch chan<- T, cancel chan struct{}) {
select {
case ch <- T{}:
case <- cancel: // 用 select 同时监听 cancel 动作
}
}
func consume(ch <-chan T, cancel chan struct{}) {
v := <-ch
err := doSomeThing(v)
if err != nil {
close(cancel) // 能够通知所有produce退出
return
}
}
for i:=0; i<10; i++ {
go produce()
}
consume()
WaitGroup 之类的可以配合着用,看自己喜欢的风格。基本上能处理好 error 场景下的资源释放,问题就不大。 对于并发的代码心存敬畏之心,哪怕用 Go,哪怕有 channel 这么好用的东西!
相关参考:
- golang: 利用unsafe操作未导出变量 https://my.oschina.net/goal/blog/193698
- 手动回收内存 https://blog.cloudflare.com/recycling-memory-buffers-in-go/
- 中文版 https://my.oschina.net/lubia/blog/175154
- Go程序内存泄漏的分析以及避免 http://www.zenlife.tk/go-leak.md
本文网址: https://golangnote.com/topic/222.html 转摘请注明来源