Golang內(nèi)存泄漏場景以及解決方案詳析
1、字符串截取
func main() { var str0 = "12345678901234567890" str1 := str0[:10] }
以上代碼,會(huì)有10字節(jié)的內(nèi)存泄漏,我們知道,str0和str1底層共享內(nèi)存,只要str1一直活躍,str0 就不會(huì)被回收,10字節(jié)的內(nèi)存被使用,剩下的10字節(jié)內(nèi)存就造成了臨時(shí)性的內(nèi)存泄漏,直到str1不再活躍
如果str0足夠大,str1截取足夠小,或者在高并發(fā)場景中頻繁使用,那么可想而知,會(huì)造成臨時(shí)性內(nèi)存泄漏,對性能產(chǎn)生極大影響。
解決方案1:string to []byte, []byte to string
func main() { var str0 = "12345678901234567890" str1 := string([]byte(str0[:10])) }
將需要截取的部分先轉(zhuǎn)換成[]byte,再轉(zhuǎn)換成string,但是這種方式會(huì)產(chǎn)生兩個(gè)10字節(jié)的臨時(shí)變量,string轉(zhuǎn)換[]byte時(shí)產(chǎn)生一個(gè)10字節(jié)臨時(shí)變量,[]byte轉(zhuǎn)換string時(shí)產(chǎn)生一個(gè)10字節(jié)的臨時(shí)變量
解決方案2:
func main() { var str0 = "12345678901234567890" str1 := (" " + str0[:10])[1:] }
這種方式仍舊會(huì)產(chǎn)生1字節(jié)的浪費(fèi)
解決方案3:strings.Builder
func main() { var str0 = "12345678901234567890" var builder strings.Builder builder.Grow(10) builder.WriteString(str0[:10]) str1 := builder.String() }
這種方式的缺點(diǎn)就是代碼量過多
解決方案4:strings.Repeat
func main() { var str0 = "12345678901234567890" str1 := strings.Repeat(str0[:10], 1) }
這種方式底層還是用到了strings.Builder,優(yōu)點(diǎn)就是將方案3進(jìn)行了封裝,代碼量得到了精簡
2、切片截取引起子切片內(nèi)存泄漏
func main() { var s0 = []int{1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := s0[:5] }
這種情況與字符串截取引起的內(nèi)存泄漏情況類似,s1活躍情況下,造成s0中部分內(nèi)存泄漏
解決方案:append
func main() { var s0 = []int{1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := append(s0[:0:0], s0[:5]...) }
append為內(nèi)置函數(shù),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] }
原切片元素為指針類型,原切片被截取后,丟失的子切片元素中的指針元素未被置空,導(dǎo)致內(nèi)存泄漏
解決方案:元素置空
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、函數(shù)數(shù)組傳參
Go數(shù)組是值類型,賦值和函數(shù)傳參都會(huì)復(fù)制整個(gè)數(shù)組
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) }
打印結(jié)果:
arrayA address: 0xc0000ae588, arrayA value: [1 2 3]
arrayB address: 0xc0000ae5a0, arrayB value: [1 2 3]
array address: 0xc0000ae5e8, array value: [1 2 3]
可以看到,三條打印的地址都不相同,說明數(shù)組是值傳遞的,這會(huì)導(dǎo)致什么問題呢?
如果我們在函數(shù)傳參的時(shí)候用到了數(shù)組傳參,且這個(gè)數(shù)組夠大(我們假設(shè)數(shù)組大小為100萬,64位機(jī)上消耗的內(nèi)存約為800w字節(jié),即8MB內(nèi)存),或者該函數(shù)短時(shí)間內(nèi)被調(diào)用N次,那么可想而知,會(huì)消耗大量內(nèi)存,對性能產(chǎn)生極大的影響,如果短時(shí)間內(nèi)分配大量內(nèi)存,而又來不及GC,那么就會(huì)產(chǎn)生臨時(shí)性的內(nèi)存泄漏,對于高并發(fā)場景相當(dāng)可怕。
解決方案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) }
打印結(jié)果:
arrayA address: 0xc00000e6a8, arrayA value: [1 2 3]
arrayB address: 0xc00000e6a8, arrayB value: [1 2 3]
array address: 0xc00000e6a8, array value: [1 2 3]
可以看到,三條打印的地址相同,說明指針是引用傳遞的 ,三個(gè)數(shù)組指向的都是同一塊內(nèi)存,就算數(shù)組很大,或者函數(shù)短時(shí)間被調(diào)用N次,也不會(huì)產(chǎn)生額外的內(nèi)存開銷,這樣會(huì)不會(huì)有隱患呢?
有,如果arrayA的指針地址發(fā)生變化,那么,arrayB和函數(shù)內(nèi)array的指針地址也隨之改變,稍不注意,容易發(fā)生bug
解決方案2:利用切片可以很好的解決以上兩個(gè)問題
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) }
打印結(jié)果:
arrayA address: 0xc00000e6a8, arrayA value: [1 2 3]
arrayB address: 0xc0000040d8, arrayB value: [1 2 3]
array address: 0xc000004108, array value: [1 2 3]
可以看到,三條打印的地址都不相同,而切片本身是一個(gè)引用類型,arrayA和arrayB底層共享內(nèi)存,不會(huì)產(chǎn)生額外內(nèi)存開銷,而且arrayA的指針地址發(fā)生改變,arrayB的指針地址也不會(huì)改變,切片的數(shù)據(jù)結(jié)構(gòu)如下:
type slice struct { array unsafe.Pointer len int cap int }
5、goroutine
“Go里面10次內(nèi)存泄漏有9次都是goroutine泄漏引起的”
有些編碼不當(dāng)?shù)那闆r下,goroutine被長期掛住,導(dǎo)致該協(xié)程中的內(nèi)存也無法被釋放,就會(huì)造成永久性的內(nèi)存泄漏。例如協(xié)程結(jié)束時(shí)協(xié)程中的channel沒有關(guān)閉,導(dǎo)致一直阻塞;例如協(xié)程中有死循環(huán);等等
我們來看下
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 } } } }
將代碼運(yùn)行起來,并利用pprof工具,在web輸入http://localhost/debug/pprof/,我們可以看到,goroutine的數(shù)量隨著時(shí)間在不斷的增加,而且絲毫沒有減少的跡象
這是因?yàn)閎reak的時(shí)候,協(xié)程中的channel并沒有關(guān)閉,導(dǎo)致協(xié)程一直存活,無法被回收
解決方案:
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,目的就是通知協(xié)程退出,這樣就避免了goroutine泄漏
6、定時(shí)器
1)time.After
func main() { ch := make(chan int) go func() { for { timerC := time.After(100 * time.Second) //timerC 每次都是重新創(chuàng)建的,什么意思呢?簡單說來,當(dāng) select 成功監(jiān)聽 ch 并進(jìn)入它的處理分支,下次循環(huán) timerC 重新創(chuàng)建了,時(shí)間肯定就重置了。 select { //如果有多個(gè) case 都可以運(yùn)行,select 會(huì)隨機(jī)公平選擇出一個(gè)執(zhí)行。其余的則不會(huì)執(zhí)行 case num := <-ch: fmt.Println("get num is", num) case <-timerC: //等價(jià)于 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) } }
以上代碼會(huì)造成內(nèi)存泄漏,time.After底層實(shí)現(xiàn)是一個(gè)timer,而定時(shí)器未到觸發(fā)時(shí)間,該定時(shí)器不會(huì)被gc回收,從而導(dǎo)致臨時(shí)性的內(nèi)存泄漏,而如果定時(shí)器一直在創(chuàng)建,那么就造成了永久性的內(nèi)存泄漏了。
解決方案:采用timer定時(shí)器
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) } }
創(chuàng)建timer定時(shí)器,每次需要啟動(dòng)定時(shí)器的時(shí)候,使用Reset方法重置定時(shí)器,這樣就不用每次都要?jiǎng)?chuàng)建新的定時(shí)器了
2)timer、ticker
在高并發(fā)、高性能場景中,使用time.NewTimer或者time.NewTicker定時(shí)器,都需要注意及時(shí)調(diào)用Stop方法來及時(shí)釋放資源,否則可能造成臨時(shí)性或者永久性的內(nèi)存泄漏。
總結(jié)
到此這篇關(guān)于Golang內(nèi)存泄漏場景以及解決方案的文章就介紹到這了,更多相關(guān)Golang內(nèi)存泄漏場景及解決內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang實(shí)現(xiàn)ftp實(shí)時(shí)傳輸文件的案例
這篇文章主要介紹了golang實(shí)現(xiàn)ftp實(shí)時(shí)傳輸文件的案例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12Go實(shí)現(xiàn)后臺任務(wù)調(diào)度系統(tǒng)的實(shí)例代碼
平常我們在開發(fā)API的時(shí)候,前端傳遞過來的大批數(shù)據(jù)需要經(jīng)過后端處理,如果后端處理的速度快,前端響應(yīng)就快,反之則很慢,影響用戶體驗(yàn),為了解決這一問題,需要我們自己實(shí)現(xiàn)后臺任務(wù)調(diào)度系統(tǒng),本文將介紹如何用Go語言實(shí)現(xiàn)后臺任務(wù)調(diào)度系統(tǒng),需要的朋友可以參考下2023-06-06Golang排列組合算法問題之全排列實(shí)現(xiàn)方法
這篇文章主要介紹了Golang排列組合算法問題之全排列實(shí)現(xiàn)方法,涉及Go語言針對字符串的遍歷及排列組合相關(guān)操作技巧,需要的朋友可以參考下2017-01-01go redis實(shí)現(xiàn)滑動(dòng)窗口限流的方式(redis版)
這篇文章主要介紹了go redis實(shí)現(xiàn)滑動(dòng)窗口限流的方式(redis版),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12golang架構(gòu)設(shè)計(jì)開閉原則手寫實(shí)現(xiàn)
這篇文章主要為大家介紹了golang架構(gòu)設(shè)計(jì)開閉原則手寫實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07