什么是内存泄漏
内存泄漏是指程序运行过程中,内存因为某些原因无法释放或没有释放。简单来讲就是,有代码占着茅坑不拉屎,让内存资源造成了浪费。如果泄漏的内存越堆越多,就会占用程序正常运行的内存。比较轻的影响是程序开始运行越来越缓慢;严重的话,可能导致大量泄漏的内存堆积,最终导致程序没有内存可以运行,最终导致 OOM (Out Of Memory,即内存溢出)。但通常来讲,内存泄漏都是极其不易发现的,所以为了保证程序的健康运行,我们需要重视如何避免写出内存泄漏的代码。
目录
slicestringtime.Ticker
slice、string 误用造成内存泄漏
a[1:3]slice
内存泄漏分析
slice
这里使用一张《Go 入门指南》的图:
xslice
sliceslice
slicesliceslicesliceslice
yxxxyxxy
验证一下
让我们使用代码验证一下:
func TestSlice(t *testing.T) {
var a []int
for i := 0; i < 100; i++ {
a = append(a, i)
}
var b = a[:10]
println(&a, &b)
println(&a[0], &b[0])
}
运行后,输出如下:
0xc000038748 0xc000038730
0xc000148400 0xc000148400
a[0]b[0]ababaababa
需要注意的是:由于 string 切片时也会共用底层数组,所以使用不当也会造成内存泄漏。
其他语言中类似的情况
slice
比如 Python,也有切片这个概念,看下面这个代码:
>>> a=[1,2,4,5]
>>> b=tab[:3]
>>> id(a[0])
140700163291672
>>> id(b[0])
140700163291672
a[0]b[0]
SubListSubListList
解决方案
TestSlice
如果我们不能保证将切片作为局部变量使用且不传递,则应该对需要的切片数据进行拷贝,防止内存泄漏。如下所示的两种方式均可:
func TestSliceSolution(t *testing.T) {
var a, b []int
for i := 0; i < 100; i++ {
a = append(a, i)
}
b = append(b, a[:10]...)
println(&a[0], &b[0])
}
//0xc000014800 0xc000020230
func TestSliceSolution2(t *testing.T) {
var a, b []int
for i := 0; i < 100; i++ {
a = append(a, i)
}
b = make([]int, 10)
copy(b, a[:10])
println(&a[0], &b[0])
}
//0xc000014800 0xc00003e6d0
time.Ticker 误用造成内存泄漏
TickerTimerTimerTickerStop
Ticker
func TestTickerNormal(t *testing.T) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
go func() {
for {
fmt.Println(<-ticker.C)
}
}()
time.Sleep(time.Second * 3)
fmt.Println("finish")
}
//2022-03-17 12:01:06.279504 +0800 CST m=+1.000922333
//2022-03-17 12:01:07.281379 +0800 CST m=+2.002815014
//finish
//2022-03-17 12:01:08.280861 +0800 CST m=+3.002314240
内存泄漏分析
Stop
StopTicker
func TestTickerUsingStop(t *testing.T) {
for i := 0; i < 100_0000; i++ {
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for i := 0; i < 3; i++ {
<-ticker.C
}
}()
}
time.Sleep(10 * time.Second)
// 以下代码用于内存分析,不重要,不需要看
f, _ := os.Create("1.prof")
defer f.Close()
runtime.GC()
_ = pprof.WriteHeapProfile(f)
log.Println("finish")
}
go tool pprof 1.proftop
Dropped 11 nodes (cum <= 2.09MB)
flat flat% sum% cum cum%
402.16MB 96.08% 96.08% 402.16MB 96.08% runtime.malg
8.67MB 2.07% 98.15% 8.67MB 2.07% runtime.allgadd
6.23MB 1.49% 99.64% 6.23MB 1.49% time.startTimer
0 0% 99.64% 6.23MB 1.49% demo.TestTickerUsingStop.func1
0 0% 99.64% 410.83MB 98.15% runtime.newproc.func1
0 0% 99.64% 410.83MB 98.15% runtime.newproc1
0 0% 99.64% 410.83MB 98.15% runtime.systemstack
0 0% 99.64% 6.23MB 1.49% time.NewTicker
StopTicker
func TestTickerWithoutUsingStop(t *testing.T) {
for i := 0; i < 100_0000; i++ {
go func() {
ticker := time.NewTicker(time.Second)
for i := 0; i < 3; i++ {
<-ticker.C
}
}()
}
time.Sleep(10 * time.Second)
// 以下代码用于内存分析,不重要,不需要看
f, _ := os.Create("2.prof")
defer f.Close()
runtime.GC()
_ = pprof.WriteHeapProfile(f)
log.Println("finish")
}
操作同上,得到输出如下:
Dropped 10 nodes (cum <= 3.04MB)
flat flat% sum% cum cum%
378.65MB 62.21% 62.21% 378.65MB 62.21% runtime.malg
210.02MB 34.51% 96.72% 219.83MB 36.12% time.NewTicker
9.81MB 1.61% 98.33% 9.81MB 1.61% time.startTimer
8.67MB 1.42% 99.75% 8.67MB 1.42% runtime.allgadd
0 0% 99.75% 219.83MB 36.12% demo.TestTickerWithoutUsingStop.func1
0 0% 99.75% 387.32MB 63.64% runtime.newproc.func1
0 0% 99.75% 387.32MB 63.64% runtime.newproc1
0 0% 99.75% 387.32MB 63.64% runtime.systemstack
• flat表示此函数分配的内存并由该函数持有
• cum表示内存是由这个函数或它调用堆栈的函数分配的
Stoptime.NewTicker
Ticker<-ticker.C
func TestTicker(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
go func() {
ticker := time.NewTicker(time.Second)
ticker.Stop() // 注意,这里先 stop 了
for i := 0; i < 3; i++ {
<-ticker.C
}
fmt.Println("ticker finish")
}()
time.Sleep(5 * time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}
// Output:
// NumGoroutine: 2
// NumGoroutine: 3
channel 误用造成内存泄漏
都说 golang 10 次内存泄漏,9 次是 go routine 泄漏。可见 go channel 内存泄漏的常见性。go channel 内存泄漏主要分两种情况,我在《老手也常误用!详解 Go channel 内存泄漏问题》这篇文章有详细讲述。这里简单说一下造成内存泄漏的代码、原因。
select-case
func TestLeakOfMemory(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
chanLeakOfMemory()
time.Sleep(time.Second * 3) // 等待 goroutine 执行,防止过早输出结果
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}
func chanLeakOfMemory() {
errCh := make(chan error)
go func() {
time.Sleep(2 * time.Second)
errCh <- errors.New("chan error") // (1)
fmt.Println("finish sending")
}()
var err error
select {
case <-time.After(time.Second): // (2) 大家也经常在这里使用 <-ctx.Done()
fmt.Println("超时")
case err = <-errCh:
if err != nil {
fmt.Println(err)
} else {
fmt.Println(nil)
}
}
}
由于 go channel 在没有缓冲队列的时候,读取 channel 默认是阻塞的,所以 (1) 处代码会阻塞,(2) 处超时后,由于没有 go routine 读取 channel ,(1) 会一直阻塞。因此输出:
NumGoroutine: 2
超时
NumGoroutine: 3
for-range
func TestLeakOfMemory2(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
chanLeakOfMemory2()
time.Sleep(time.Second * 3) // 等待 goroutine 执行,防止过早输出结果
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}
func chanLeakOfMemory2() {
ich := make(chan int, 100)
// sender
go func() {
defer close(ich)
for i := 0; i < 10000; i++ {
ich <- i // (2)
time.Sleep(time.Millisecond) // 控制一下,别发太快
}
}()
// receiver
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
for i := range ich {
if ctx.Err() != nil { // (1)
fmt.Println(ctx.Err())
return
}
fmt.Println(i)
}
}()
}
// Output:
// NumGoroutine: 2
// 0
// 1
// ...(省略)...
// 789
// context deadline exceeded
// NumGoroutine: 3
ctx.Err() != nilich
解决方案
如果接收者需要在 channel 关闭之前提前退出,为防止内存泄漏,在发送者与接收者发送次数是一对一时,应设置 channel 缓冲队列为 1;在发送者与接收者的发送次数是多对多时,应使用专门的 stop channel 通知发送者关闭相应 channel。
由于篇幅限制,更详细的内容可以看《老手也常误用!详解 Go channel 内存泄漏问题》这篇文章。
总结
以上造成内存泄漏的示例看起来似乎都是小问题,单个示例泄漏的内存不多。但要注意,我们的上述代码可能被写在一个 go routine 中,如果每次访问,都是用一个 go routine 处理(比如后端中,每有一个请求,就会创建一个 go routine 来处理),那么是不是访问的次数越多,泄漏的内存越多。内存泄漏正是由这种看似不起眼的小问题造成的。如果放任不管或不重视,最终造成的结果就是业务频繁宕机、卡顿等。所以我们在业务中应该极其重视。
参考文章
• 一些可能的内存泄漏场景:https://gfw.go101.org/article/memory-leaking.html
引用链接
[1]