首頁  >  文章  >  後端開發  >  golang記憶體洩漏原因有哪些

golang記憶體洩漏原因有哪些

青灯夜游
青灯夜游原創
2023-01-10 17:45:482339瀏覽

洩漏原因有:1、time.After()的使用,每次time.After(duration x)會產生NewTimer(),在duration x到期前,新建立的timer不會被GC ,到期後才會GC;2、time.NewTicker資源未及時釋放;3、select阻塞;4、channel阻塞;5、申請過多的goroutine、goroutine阻塞;6、slice引起的等。

golang記憶體洩漏原因有哪些

本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。

golang容易導致記憶體洩漏的幾種情況

#1. 定時器使用不當

1.1 time.After()的使用

預設的time.After()是會有記憶體洩漏問題的,因為每次time.After(duration x)會產生NewTimer( ),在duration x到期之前,新創建的timer不會被GC,到期之後才會GC。

隨著時間推移,尤其是duration x很大的話,會產生記憶體外洩的問題,應特別注意

for true {
	select {
	case <-time.After(time.Minute * 3):
    // do something
  default:
		time.Sleep(time.Duration(1) * time.Second)
	}
}

為了保險起見,使用NewTimer()或NewTicker()代替的方式主動釋放資源,兩者的差異請自行查閱或看我往期文章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時需要手動呼叫Stop()方法釋放資源,否則將會造成永久性的記憶體洩漏

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分支進行處理,最終會導致記憶體洩漏

2.1 導致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的消費處導致記憶體洩漏

2.2 迴圈空轉導致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迴圈條件一旦命中default則會出現循環空轉的情況,並最終導致CPU暴漲

#3. channel阻塞

channel阻塞主要分為寫入阻塞和讀取阻塞兩種情況

空channel

func channelTest() {
  	//声明未初始化的channel读写都会阻塞
    var c chan int
  	//向channel中写数据
    go func() {
        c <- 1
        fmt.Println("g1 send succeed")
        time.Sleep(1 * time.Second)
    }()
  	//从channel中读数据
    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个协程向channel中写数据
    for i := 0; i < 10; i++ {
        go func() {
            <- c
            fmt.Println("g1 receive succeed")
            time.Sleep(1 * time.Second)
        }()
    }
  	//1个协程丛channel读数据
    go func() {
        c <- 1
        fmt.Println("g2 send succeed")
        time.Sleep(1 * time.Second)
    }()
  	//会有写的9个协程阻塞得不到释放
    time.Sleep(10 * time.Second)
}

 

  • 有緩衝的channel因為緩衝區滿了,寫入操作阻塞
func channelTest() {
    var c = make(chan int, 8)
  	//10个协程向channel中写数据
    for i := 0; i < 10; i++ {
        go func() {
            <- c
            fmt.Println("g1 receive succeed")
            time.Sleep(1 * time.Second)
        }()
    }
  	//1个协程丛channel读数据
    go func() {
        c <- 1
        fmt.Println("g2 send succeed")
        time.Sleep(1 * time.Second)
    }()
  	//会有写的几个协程阻塞写不进去
    time.Sleep(10 * time.Second)
}

讀取阻塞

  • 期待從channel讀數據,結果沒有goroutine往進寫資料
func channelTest() {
   var c = make(chan int)
  //1个协程向channel中写数据
  go func() {
    <- c
    fmt.Println("g1 receive succeed")
    time.Sleep(1 * time.Second)
  }()
  //10个协程丛channel读数据
  for i := 0; i < 10; i++ {
    go func() {
        c <- 1
        fmt.Println("g2 send succeed")
        time.Sleep(1 * time.Second)
    }()
  }
  //会有读的9个协程阻塞得不到释放
  time.Sleep(10 * time.Second)
}

#4. goroutine導致的記憶體洩漏

4.1 申請過多的goroutine

#例如在for循環中申請過多的goroutine來不及釋放導致記憶體洩漏

#4.2 goroutine阻塞

4.2. 1 I/O問題

I/O連線未設定逾時時間,導致goroutine一直在等待,程式碼會一直阻塞。

4.2.2 互斥鎖未釋放

goroutine無法取得到鎖定資源,導致goroutine阻塞

//协程拿到锁未释放,其他协程获取锁会阻塞
func mutexTest() {
    mutex := sync.Mutex{}
    for i := 0; i < 10; i++ {
        go func() {
            mutex.Lock()
            fmt.Printf("%d goroutine get mutex", i)
      			//模拟实际开发中的操作耗时
            time.Sleep(100 * time.Millisecond)
        }()
    }
    time.Sleep(10 * time.Second)
}
#4.2.3 死鎖

當程式死鎖時其他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")
    }()
  	//其余协程获取锁都会失败
    go func() {
        m1.Lock()
        fmt.Println("g3 get m1")
    }()
    time.Sleep(10 * time.Second)
}
4.2. 4 waitgroup使用不當

waitgroup的Add、Done和wait數量不匹配會導致wait一直在等待

##5. slice 引起的內存洩漏

當兩個slice 共享位址,其中一個為全域變量,另一個也無法被GC;

append slice 後一直使用,沒有進行清理。

var a []int
 
func test(b []int) {
        a = b[:3]
        return
}

6. 陣列的值傳遞

#由於陣列時Golang的基本資料類型,每個陣列佔用不通的記憶體空間,生命週期互不干擾,很難出現記憶體洩漏的情況,但是數組作為形參傳輸時,遵循的時值拷貝,如果函數被多個goroutine調用且數組過大時,則會導致記憶體使用激增。

//统计nums中target出现的次数
func countTarget(nums [1000000]int, target int) int {
    num := 0
    for i := 0; i < len(nums) && nums[i] == target; i++ {
        num++
    }
    return num
}

因此對於大數組放在形參場景下通常使用切片或指標進行傳遞,避免短時間的記憶體使用激增、

【相關推薦:

Go影片教學程式設計教學#

以上是golang記憶體洩漏原因有哪些的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn