泄漏原因有:1、time.After()的使用,每次time.After(duration x)會產(chǎn)生NewTimer(),在duration x到期之前,新創(chuàng)建的timer不會被GC,到期之后才會GC;2、time.NewTicker資源未及時釋放;3、select阻塞;4、channel阻塞;5、申請過多的goroutine、goroutine阻塞;6、slice引起的等。
本教程操作環(huán)境:windows7系統(tǒng)、GO 1.18版本、Dell G3電腦。
golang容易導(dǎo)致內(nèi)存泄漏的幾種情況
1. 定時器使用不當(dāng)
1.1 time.After()的使用
默認(rèn)的time.After()是會有內(nèi)存泄露問題的,因為每次time.After(duration x)會產(chǎn)生NewTimer(),在duration x到期之前,新創(chuàng)建的timer不會被GC,到期之后才會GC。
隨著時間推移,尤其是duration x很大的話,會產(chǎn)生內(nèi)存泄露的問題,應(yīng)特別注意
for true { select { case <-time.After(time.Minute * 3): // do something default: time.Sleep(time.Duration(1) * time.Second) } }
為了保險起見,使用NewTimer()或者NewTicker()代替的方式主動釋放資源,兩者的區(qū)別請自行查閱或看我往期文章https://blog.csdn.net/weixin_38299404/article/details/119352884
timer := time.NewTicker(time.Duration(2) * time.Second) defer timer.Stop() for true { select { case <-timer.C: // do something default: time.Sleep(time.Duration(1) * time.Second) } }
1.2 time.NewTicker資源未及時釋放
在使用time.NewTicker時需要手動調(diào)用Stop()方法釋放資源,否則將會造成永久性的內(nèi)存泄漏
timer := time.NewTicker(time.Duration(2) * time.Second) // defer timer.Stop() for true { select { case <-timer.C: // do something default: time.Sleep(time.Duration(1) * time.Second) } }
2. select阻塞
使用select時如果有case沒有覆蓋完全的情況且沒有default分支進(jìn)行處理,最終會導(dǎo)致內(nèi)存泄漏
2.1 導(dǎo)致goroutine阻塞的情況
func main() { ch1 := make(chan int) ch2 := make(chan int) ch3 := make(chan int) go Getdata("https://www.baidu.com",ch1) go Getdata("https://www.baidu.com",ch2) go Getdata("https://www.baidu.com",ch3) select{ case v:=<- ch1: fmt.Println(v) case v:=<- ch2: fmt.Println(v) } }
上述這種情況會阻塞在ch3的消費處導(dǎo)致內(nèi)存泄漏
2.2 循環(huán)空轉(zhuǎn)導(dǎo)致CPU暴漲
func main() { fmt.Println("main start") msgList := make(chan int, 100) go func() { for { select { case <-msgList: default: } } }() c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, os.Kill) s := <-c fmt.Println("main exit.get signal:", s) }
上述for循環(huán)條件一旦命中default則會出現(xiàn)循環(huán)空轉(zhuǎn)的情況,并最終導(dǎo)致CPU暴漲
3. channel阻塞
channel阻塞主要分為寫阻塞和讀阻塞兩種情況
空channel
func channelTest() { //聲明未初始化的channel讀寫都會阻塞 var c chan int //向channel中寫數(shù)據(jù) go func() { c <- 1 fmt.Println("g1 send succeed") time.Sleep(1 * time.Second) }() //從channel中讀數(shù)據(jù) go func() { <-c fmt.Println("g2 receive succeed") time.Sleep(1 * time.Second) }() time.Sleep(10 * time.Second) }
寫阻塞
- 無緩沖channel的阻塞通常是寫操作因為沒有讀而阻塞
func channelTest() { var c = make(chan int) //10個協(xié)程向channel中寫數(shù)據(jù) for i := 0; i < 10; i++ { go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() } //1個協(xié)程叢channel讀數(shù)據(jù) go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() //會有寫的9個協(xié)程阻塞得不到釋放 time.Sleep(10 * time.Second) }
- 有緩沖的channel因為緩沖區(qū)滿了,寫操作阻塞
func channelTest() { var c = make(chan int, 8) //10個協(xié)程向channel中寫數(shù)據(jù) for i := 0; i < 10; i++ { go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() } //1個協(xié)程叢channel讀數(shù)據(jù) go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() //會有寫的幾個協(xié)程阻塞寫不進(jìn)去 time.Sleep(10 * time.Second) }
讀阻塞
- 期待從channel讀數(shù)據(jù),結(jié)果沒有g(shù)oroutine往進(jìn)寫數(shù)據(jù)
func channelTest() { var c = make(chan int) //1個協(xié)程向channel中寫數(shù)據(jù) go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() //10個協(xié)程叢channel讀數(shù)據(jù) for i := 0; i < 10; i++ { go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() } //會有讀的9個協(xié)程阻塞得不到釋放 time.Sleep(10 * time.Second) }
4. goroutine導(dǎo)致的內(nèi)存泄漏
4.1 申請過多的goroutine
例如在for循環(huán)中申請過多的goroutine來不及釋放導(dǎo)致內(nèi)存泄漏
4.2 goroutine阻塞
4.2.1 I/O問題
I/O連接未設(shè)置超時時間,導(dǎo)致goroutine一直在等待,代碼會一直阻塞。
4.2.2 互斥鎖未釋放
goroutine無法獲取到鎖資源,導(dǎo)致goroutine阻塞
//協(xié)程拿到鎖未釋放,其他協(xié)程獲取鎖會阻塞 func mutexTest() { mutex := sync.Mutex{} for i := 0; i < 10; i++ { go func() { mutex.Lock() fmt.Printf("%d goroutine get mutex", i) //模擬實際開發(fā)中的操作耗時 time.Sleep(100 * time.Millisecond) }() } time.Sleep(10 * time.Second) }
4.2.3 死鎖
當(dāng)程序死鎖時其他goroutine也會阻塞
func mutexTest() { m1, m2 := sync.Mutex{}, sync.RWMutex{} //g1得到鎖1去獲取鎖2 go func() { m1.Lock() fmt.Println("g1 get m1") time.Sleep(1 * time.Second) m2.Lock() fmt.Println("g1 get m2") }() //g2得到鎖2去獲取鎖1 go func() { m2.Lock() fmt.Println("g2 get m2") time.Sleep(1 * time.Second) m1.Lock() fmt.Println("g2 get m1") }() //其余協(xié)程獲取鎖都會失敗 go func() { m1.Lock() fmt.Println("g3 get m1") }() time.Sleep(10 * time.Second) }
4.2.4 waitgroup使用不當(dāng)
waitgroup的Add、Done和wait數(shù)量不匹配會導(dǎo)致wait一直在等待
5. slice 引起的內(nèi)存泄漏
當(dāng)兩個slice 共享地址,其中一個為全局變量,另一個也無法被GC;
append slice 后一直使用,沒有進(jìn)行清理。
var a []int func test(b []int) { a = b[:3] return }
6. 數(shù)組的值傳遞
由于數(shù)組時Golang的基本數(shù)據(jù)類型,每個數(shù)組占用不通的內(nèi)存空間,生命周期互不干擾,很難出現(xiàn)內(nèi)存泄漏的情況,但是數(shù)組作為形參傳輸時,遵循的時值拷貝,如果函數(shù)被多個goroutine調(diào)用且數(shù)組過大時,則會導(dǎo)致內(nèi)存使用激增。
//統(tǒng)計nums中target出現(xiàn)的次數(shù) func countTarget(nums [1000000]int, target int) int { num := 0 for i := 0; i < len(nums) && nums[i] == target; i++ { num++ } return num }
因此對于大數(shù)組放在形參場景下通常使用切片或者指針進(jìn)行傳遞,避免短時間的內(nèi)存使用激增、
【