Golang内存泄漏场景以及解决方案详析
8023之永恒 人气:01、字符串截取
func main() { var str0 = "12345678901234567890" str1 := str0[:10] }
以上代码,会有10字节的内存泄漏,我们知道,str0和str1底层共享内存,只要str1一直活跃,str0 就不会被回收,10字节的内存被使用,剩下的10字节内存就造成了临时性的内存泄漏,直到str1不再活跃
如果str0足够大,str1截取足够小,或者在高并发场景中频繁使用,那么可想而知,会造成临时性内存泄漏,对性能产生极大影响。
解决方案1:string to []byte, []byte to string
func main() { var str0 = "12345678901234567890" str1 := string([]byte(str0[:10])) }
将需要截取的部分先转换成[]byte,再转换成string,但是这种方式会产生两个10字节的临时变量,string转换[]byte时产生一个10字节临时变量,[]byte转换string时产生一个10字节的临时变量
解决方案2:
func main() { var str0 = "12345678901234567890" str1 := (" " + str0[:10])[1:] }
这种方式仍旧会产生1字节的浪费
解决方案3:strings.Builder
func main() { var str0 = "12345678901234567890" var builder strings.Builder builder.Grow(10) builder.WriteString(str0[:10]) str1 := builder.String() }
这种方式的缺点就是代码量过多
解决方案4:strings.Repeat
func main() { var str0 = "12345678901234567890" str1 := strings.Repeat(str0[:10], 1) }
这种方式底层还是用到了strings.Builder,优点就是将方案3进行了封装,代码量得到了精简
2、切片截取引起子切片内存泄漏
func main() { var s0 = []int{1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := s0[:5] }
这种情况与字符串截取引起的内存泄漏情况类似,s1活跃情况下,造成s0中部分内存泄漏
解决方案:append
func main() { var s0 = []int{1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := append(s0[:0:0], s0[:5]...) }
append为内置函数,go源码src/builtin/builtin.go中释义:
// The append built-in function appends elements to the end of a slice. If // it has sufficient capacity, the destination is resliced to accommodate the // new elements. If it does not, a new underlying array will be allocated. // Append returns the updated slice. It is therefore necessary to store the // result of append, often in the variable holding the slice itself: // slice = append(slice, elem1, elem2) // slice = append(slice, anotherSlice...) // As a special case, it is legal to append a string to a byte slice, like this: // slice = append([]byte("hello "), "world"...) func append(slice []Type, elems ...Type) []Type
3、没有重置丢失的子切片元素中的指针
func main() { var s0 = []*int{new(int), new(int), new(int), new(int), new(int)} s1 := s0[1:3] }
原切片元素为指针类型,原切片被截取后,丢失的子切片元素中的指针元素未被置空,导致内存泄漏
解决方案:元素置空
func main() { var s0 = []*int{new(int), new(int), new(int), new(int), new(int)} s0[0], s0[3], s0[4] = nil, nil, nil s1 := s0[1:3] }
4、函数数组传参
Go数组是值类型,赋值和函数传参都会复制整个数组
func main() { var arrayA = [3]int{1, 2, 3} var arrayB = [3]int{} arrayB = arrayA fmt.Printf("arrayA address: %p, arrayA value: %+v\n", &arrayA, arrayA) fmt.Printf("arrayB address: %p, arrayB value: %+v\n", &arrayB, arrayB) array(arrayA) } func array(array [3]int) { fmt.Printf("array address: %p, array value: %+v\n", &array, array) }
打印结果:
arrayA address: 0xc0000ae588, arrayA value: [1 2 3]
arrayB address: 0xc0000ae5a0, arrayB value: [1 2 3]
array address: 0xc0000ae5e8, array value: [1 2 3]
可以看到,三条打印的地址都不相同,说明数组是值传递的,这会导致什么问题呢?
如果我们在函数传参的时候用到了数组传参,且这个数组够大(我们假设数组大小为100万,64位机上消耗的内存约为800w字节,即8MB内存),或者该函数短时间内被调用N次,那么可想而知,会消耗大量内存,对性能产生极大的影响,如果短时间内分配大量内存,而又来不及GC,那么就会产生临时性的内存泄漏,对于高并发场景相当可怕。
解决方案1:采用指针传递
func main() { var arrayA = [3]int{1, 2, 3} var arrayB = &arrayA fmt.Printf("arrayA address: %p, arrayA value: %+v\n", &arrayA, arrayA) fmt.Printf("arrayB address: %p, arrayB value: %+v\n", arrayB, *arrayB) arrayP(&arrayA) } func arrayP(array *[3]int) { fmt.Printf("array address: %p, array value: %+v\n", array, *array) }
打印结果:
arrayA address: 0xc00000e6a8, arrayA value: [1 2 3]
arrayB address: 0xc00000e6a8, arrayB value: [1 2 3]
array address: 0xc00000e6a8, array value: [1 2 3]
可以看到,三条打印的地址相同,说明指针是引用传递的 ,三个数组指向的都是同一块内存,就算数组很大,或者函数短时间被调用N次,也不会产生额外的内存开销,这样会不会有隐患呢?
有,如果arrayA的指针地址发生变化,那么,arrayB和函数内array的指针地址也随之改变,稍不注意,容易发生bug
解决方案2:利用切片可以很好的解决以上两个问题
func main() { var arrayA = [3]int{1, 2, 3} var arrayB = arrayA[:] fmt.Printf("arrayA address: %p, arrayA value: %+v\n", &arrayA, arrayA) fmt.Printf("arrayB address: %p, arrayB value: %+v\n", &arrayB, arrayB) arrayS(arrayB) } func arrayS(array []int) { fmt.Printf("array address: %p, array value: %+v\n", &array, array) }
打印结果:
arrayA address: 0xc00000e6a8, arrayA value: [1 2 3]
arrayB address: 0xc0000040d8, arrayB value: [1 2 3]
array address: 0xc000004108, array value: [1 2 3]
可以看到,三条打印的地址都不相同,而切片本身是一个引用类型,arrayA和arrayB底层共享内存,不会产生额外内存开销,而且arrayA的指针地址发生改变,arrayB的指针地址也不会改变,切片的数据结构如下:
type slice struct { array unsafe.Pointer len int cap int }
5、goroutine
“Go里面10次内存泄漏有9次都是goroutine泄漏引起的”
有些编码不当的情况下,goroutine被长期挂住,导致该协程中的内存也无法被释放,就会造成永久性的内存泄漏。例如协程结束时协程中的channel没有关闭,导致一直阻塞;例如协程中有死循环;等等
我们来看下
func main() { ticker := time.NewTicker(time.Second * 1) for { <-ticker.C ch := make(chan int) go func() { for i := 0; i < 100; i++ { ch <- i } }() for v := range ch { if v == 50 { break } } } }
将代码运行起来,并利用pprof工具,在web输入http://localhost/debug/pprof/,我们可以看到,goroutine的数量随着时间在不断的增加,而且丝毫没有减少的迹象
这是因为break的时候,协程中的channel并没有关闭,导致协程一直存活,无法被回收
解决方案:
func main() { ticker := time.NewTicker(time.Second * 1) for { <-ticker.C cxt, cancel := context.WithCancel(context.Background()) ch := make(chan int) go func(cxt context.Context) { for i := 0; i < 100; i++ { select { case <-cxt.Done(): return case ch <- i: } } }(cxt) for v := range ch { if v == 50 { cancel() break } } } }
利用context,在break之前cancel,目的就是通知协程退出,这样就避免了goroutine泄漏
6、定时器
1)time.After
func main() { ch := make(chan int) go func() { for { timerC := time.After(100 * time.Second) //timerC 每次都是重新创建的,什么意思呢?简单说来,当 select 成功监听 ch 并进入它的处理分支,下次循环 timerC 重新创建了,时间肯定就重置了。 select { //如果有多个 case 都可以运行,select 会随机公平选择出一个执行。其余的则不会执行 case num := <-ch: fmt.Println("get num is", num) case <-timerC: //等价于 case <-time.After(100 * time.Second) fmt.Println("time's up!!!") //done<-true } } }() for i := 1; i < 100000; i++ { ch <- i time.Sleep(time.Millisecond) } }
以上代码会造成内存泄漏,time.After底层实现是一个timer,而定时器未到触发时间,该定时器不会被gc回收,从而导致临时性的内存泄漏,而如果定时器一直在创建,那么就造成了永久性的内存泄漏了。
解决方案:采用timer定时器
func main() { ch := make(chan int) go func() { timer := time.NewTimer(100 * time.Second) defer timer.Stop() for { timer.Reset(100 * time.Second) select { case num := <-ch: fmt.Println("get num is", num) case <-timer.C: fmt.Println("time's up!!!") } } }() for i := 1; i < 100000; i++ { ch <- i time.Sleep(time.Millisecond) } }
创建timer定时器,每次需要启动定时器的时候,使用Reset方法重置定时器,这样就不用每次都要创建新的定时器了
2)timer、ticker
在高并发、高性能场景中,使用time.NewTimer或者time.NewTicker定时器,都需要注意及时调用Stop方法来及时释放资源,否则可能造成临时性或者永久性的内存泄漏。
总结
加载全部内容