go語言內存泄漏的常見形式
go語言內存泄漏
子字符串導致的內存泄漏
使用自動垃圾回收的語言進行編程時,通常我們無需擔心內存泄漏的問題,因為運行時會定期回收未使用的內存。但是如果你以為這樣就完事大吉了,哪里就大錯特措了。
因為,雖然go中并未對字符串時候共享底層內存塊進行規(guī)定,但go語言編譯器/運行時默認情況下允許字符串共享底層內存塊,直到原先的字符串指向的內存被修改才會進行寫時復制,這是一個很好的設計,既能節(jié)省內存,又能節(jié)省CPU資源,但有時也會導致"內存泄漏"。
例如如下代碼,一旦調用demo就會導致將近1M內存的泄漏,因為s0只使用了50字節(jié),但是會導致1M的內存一直無法被回收,這些內存會一直持續(xù)到下次s0被修改的時候才會被釋放掉。
var s0 string // a package-level variable // A demo purpose function. func f(s1 string) { s0 = s1[:50] // Now, s0 shares the same underlying memory block // with s1. Although s1 is not alive now, but s0 // is still alive, so the memory block they share // couldn't be collected, though there are only 50 // bytes used in the block and all other bytes in // the block become unavailable. } func demo() { s := createStringWithLengthOnHeap(1 << 20) // 1M bytes f(s) }
為了避免這種內存泄漏,我們可以使用[]byte來替代原先的1M大小的內存,不過這樣會有兩次50字節(jié)的內存重復
func f(s1 string) { s0 = string([]byte(s1[:50])) }
當然我們也可以利用go編譯器的優(yōu)化來避免不必要的重復,只需要浪費一個字節(jié)內存就行
func f(s1 string) { s0 = (" " + s1[:50])[1:] }
上述方法的缺點是編譯器優(yōu)化以后可能會失效,并且其他編譯器可能無法提供該優(yōu)化
避免此類內存泄漏的第三種方法是利用 Go 1.10 以來支持的 strings.Builder
。
import "strings" func f(s1 string) { var b strings.Builder b.Grow(50) b.WriteString(s1[:50]) s0 = b.String() }
從 Go 1.18 開始, strings
標準庫包中新增了 Clone
函數,這成為了完成這項工作的最佳方式。
子切片導致的內存泄漏
同樣場景下,切片也會導致內存的浪費
與子字符串類似,子切片也可能導致某種內存泄漏。在下面的代碼中,調用 g
函數后,保存 s1
元素的內存塊所占用的大部分內存將會丟失(如果沒有其他值引用該內存塊)。
var s0 []int func g(s1 []int) { // Assume the length of s1 is much larger than 30. s0 = s1[len(s1)-30:] }
如果我們想避免這種內存泄漏,我們必須復制 s0
的 30 個元素,這樣 s0
的活躍性就不會阻止收集承載 s1
元素的內存塊。
func g(s1 []int) { s0 = make([]int, 30) copy(s0, s1[len(s1)-30:]) // Now, the memory block hosting the elements // of s1 can be collected if no other values // are referencing the memory block. }
未重置子切片指針導致的內存泄漏
在下面的代碼中,調用 h
函數后,為切片 s
的第一個和最后一個元素分配的內存塊將丟失。
func h() []*int { s := []*int{new(int), new(int), new(int), new(int)} // do something with s ... // 返回一個從1開始,不能到索引3的新切片, 也就是 s[1], s[2] return s[1:3:3] }
只要返回的切片仍然有效,它就會阻止收集 s
的任何元素,從而阻止收集為 s
的第一個和最后一個元素引用的兩個 int
值分配的兩個內存塊。
如果我們想避免這種內存泄漏,我們必須重置丟失元素中存儲的指針。
func h() []*int { s := []*int{new(int), new(int), new(int), new(int)} // do something with s ... // Reset pointer values. s[0], s[len(s)-1] = nil, nil return s[1:3:3] }
掛起Goroutine導致的內存泄漏
有時,Go 程序中的某些 goroutine 可能會永遠處于阻塞狀態(tài)。這樣的 goroutine 被稱為掛起的 goroutine。Go 運行時不會終止掛起的 goroutine,因此為掛起的 goroutine 分配的資源(以及它們引用的內存塊)永遠不會被垃圾回收。
Go 運行時不會殺死掛起的 Goroutine 有兩個原因。一是 Go 運行時有時很難判斷一個阻塞的 Goroutine 是否會被永久阻塞。二是我們有時會故意讓 Goroutine 掛起。例如,有時我們可能會讓 Go 程序的主 Goroutine 掛起,以避免程序退出。
如果不停止time.Ticker也會導致內存泄漏
當 time.Timer
值不再使用時,它會在一段時間后被垃圾回收。但 time.Ticker
值則不然。我們應該在 time.Ticker
值不再使用時停止它。
不正確地使用終結器會導致真正的內存泄漏
為屬于循環(huán)引用組的成員值設置終結器(finalizer)可能會阻止為該循環(huán)引用組分配的所有內存塊被回收。這是真正的內存泄漏,不是某種假象。
例如,在調用并退出以下函數后,分配給 x
和 y
的內存塊不能保證在未來的垃圾收集中被收集。
func memoryLeaking() { type T struct { v [1<<20]int t *T } var finalizer = func(t *T) { fmt.Println("finalizer called") } var x, y T // The SetFinalizer call makes x escape to heap. runtime.SetFinalizer(&x, finalizer) // The following line forms a cyclic reference // group with two members, x and y. // This causes x and y are not collectable. x.t, y.t = &y, &x // y also escapes to heap. }
因此,請避免為循環(huán)引用組中的值設置終結器。
延遲函數調用導致的某種資源泄漏
非常大的延遲調用堆棧也可能會消耗大量內存,并且如果某些調用延遲太多,某些資源可能無法及時釋放。
例如,如果在調用以下函數時需要處理許多文件,那么在函數退出之前將有大量文件處理程序無法釋放。
func writeManyFiles(files []File) error { for _, file := range files { f, err := os.Open(file.path) if err != nil { return err } defer f.Close() _, err = f.WriteString(file.content) if err != nil { return err } err = f.Sync() if err != nil { return err } } return nil }
對于這種情況,我們可以使用匿名函數來封裝延遲調用,以便延遲函數調用能夠更早地執(zhí)行。例如,上面的函數可以重寫并改進為
func writeManyFiles(files []File) error { for _, file := range files { if err := func() error { f, err := os.Open(file.path) if err != nil { return err } // The close method will be called at // the end of the current loop step. defer f.Close() _, err = f.WriteString(file.content) if err != nil { return err } return f.Sync() }(); err != nil { return err } } return nil }
當然不要犯以下錯誤,需要有些同學將需要延時調用的函數字節(jié)省略,導致資源泄漏
_, err := os.Open(file.path)
如果是http請求,還會導致服務端擠壓大量的連接無法釋放
到此這篇關于go語言內存泄漏的常見形式的文章就介紹到這了,更多相關go語言內存泄漏內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Go語言中strings.HasPrefix、strings.Split、strings.SplitN()?函數
本文主要介紹了Go語言中strings.HasPrefix、strings.Split、strings.SplitN()函數,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-08-08