go語言內存泄漏
子字符串導致的內存泄漏
使用自動垃圾回收的語言進行編程時,通常我們無需擔心內存泄漏的問題,因為運行時會定期回收未使用的內存。但是如果你以為這樣就完事大吉了,哪里就大錯特措了。
因為,雖然go中并未對字符串時候共享底層內存塊進行規定,但go語言編譯器/運行時默認情況下允許字符串共享底層內存塊,直到原先的字符串指向的內存被修改才會進行寫時復制,這是一個很好的設計,既能節省內存,又能節省CPU資源,但有時也會導致"內存泄漏"。
例如如下代碼,一旦調用demo就會導致將近1M內存的泄漏,因為s0只使用了50字節,但是會導致1M的內存一直無法被回收,這些內存會一直持續到下次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 bytesf(s)
}
為了避免這種內存泄漏,我們可以使用[]byte來替代原先的1M大小的內存,不過這樣會有兩次50字節的內存重復
func f(s1 string) {s0 = string([]byte(s1[:50]))
}
當然我們也可以利用go編譯器的優化來避免不必要的重復,只需要浪費一個字節內存就行
func f(s1 string) {s0 = (" " + s1[:50])[1:]
}
上述方法的缺點是編譯器優化以后可能會失效,并且其他編譯器可能無法提供該優化
避免此類內存泄漏的第三種方法是利用 Go 1.10 以來支持的 strings.Builder
。
import "strings"func f(s1 string) {var b strings.Builderb.Grow(50)b.WriteString(s1[:50])s0 = b.String()
}
從 Go 1.18 開始, strings
標準庫包中新增了 Clone
函數,這成為了完成這項工作的最佳方式。
子切片導致的內存泄漏
同樣場景下,切片也會導致內存的浪費
與子字符串類似,子切片也可能導致某種內存泄漏。在下面的代碼中,調用 g
函數后,保存 s1
元素的內存塊所占用的大部分內存將會丟失(如果沒有其他值引用該內存塊)。
var s0 []intfunc 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, nilreturn s[1:3:3]
}
掛起Goroutine導致的內存泄漏
有時,Go 程序中的某些 goroutine 可能會永遠處于阻塞狀態。這樣的 goroutine 被稱為掛起的 goroutine。Go 運行時不會終止掛起的 goroutine,因此為掛起的 goroutine 分配的資源(以及它們引用的內存塊)永遠不會被垃圾回收。
Go 運行時不會殺死掛起的 Goroutine 有兩個原因。一是 Go 運行時有時很難判斷一個阻塞的 Goroutine 是否會被永久阻塞。二是我們有時會故意讓 Goroutine 掛起。例如,有時我們可能會讓 Go 程序的主 Goroutine 掛起,以避免程序退出。
如果不停止time.Ticker
也會導致內存泄漏
當 time.Timer
值不再使用時,它會在一段時間后被垃圾回收。但 time.Ticker
值則不然。我們應該在 time.Ticker
值不再使用時停止它。
不正確地使用終結器會導致真正的內存泄漏
為屬于循環引用組的成員值設置終結器(finalizer)可能會阻止為該循環引用組分配的所有內存塊被回收。這是真正的內存泄漏,不是某種假象。
例如,在調用并退出以下函數后,分配給 x
和 y
的內存塊不能保證在未來的垃圾收集中被收集。
func memoryLeaking() {type T struct {v [1<<20]intt *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.
}
因此,請避免為循環引用組中的值設置終結器。
延遲函數調用導致的某種資源泄漏
非常大的延遲調用堆棧也可能會消耗大量內存,并且如果某些調用延遲太多,某些資源可能無法及時釋放。
例如,如果在調用以下函數時需要處理許多文件,那么在函數退出之前將有大量文件處理程序無法釋放。
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
}
對于這種情況,我們可以使用匿名函數來封裝延遲調用,以便延遲函數調用能夠更早地執行。例如,上面的函數可以重寫并改進為
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
}
當然不要犯以下錯誤,需要有些同學將需要延時調用的函數字節省略,導致資源泄漏
_, err := os.Open(file.path)
如果是http請求,還會導致服務端擠壓大量的連接無法釋放