go語言內(nèi)存泄漏的常見形式
go語言內(nèi)存泄漏
子字符串導(dǎo)致的內(nèi)存泄漏
使用自動垃圾回收的語言進(jìn)行編程時,通常我們無需擔(dān)心內(nèi)存泄漏的問題,因為運(yùn)行時會定期回收未使用的內(nèi)存。但是如果你以為這樣就完事大吉了,哪里就大錯特措了。
因為,雖然go中并未對字符串時候共享底層內(nèi)存塊進(jìn)行規(guī)定,但go語言編譯器/運(yùn)行時默認(rèn)情況下允許字符串共享底層內(nèi)存塊,直到原先的字符串指向的內(nèi)存被修改才會進(jìn)行寫時復(fù)制,這是一個很好的設(shè)計,既能節(jié)省內(nèi)存,又能節(jié)省CPU資源,但有時也會導(dǎo)致"內(nèi)存泄漏"。
例如如下代碼,一旦調(diào)用demo就會導(dǎo)致將近1M內(nèi)存的泄漏,因為s0只使用了50字節(jié),但是會導(dǎo)致1M的內(nèi)存一直無法被回收,這些內(nèi)存會一直持續(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) }
為了避免這種內(nèi)存泄漏,我們可以使用[]byte來替代原先的1M大小的內(nèi)存,不過這樣會有兩次50字節(jié)的內(nèi)存重復(fù)
func f(s1 string) { s0 = string([]byte(s1[:50])) }
當(dāng)然我們也可以利用go編譯器的優(yōu)化來避免不必要的重復(fù),只需要浪費一個字節(jié)內(nèi)存就行
func f(s1 string) { s0 = (" " + s1[:50])[1:] }
上述方法的缺點是編譯器優(yōu)化以后可能會失效,并且其他編譯器可能無法提供該優(yōu)化
避免此類內(nèi)存泄漏的第三種方法是利用 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
標(biāo)準(zhǔn)庫包中新增了 Clone
函數(shù),這成為了完成這項工作的最佳方式。
子切片導(dǎo)致的內(nèi)存泄漏
同樣場景下,切片也會導(dǎo)致內(nèi)存的浪費
與子字符串類似,子切片也可能導(dǎo)致某種內(nèi)存泄漏。在下面的代碼中,調(diào)用 g
函數(shù)后,保存 s1
元素的內(nèi)存塊所占用的大部分內(nèi)存將會丟失(如果沒有其他值引用該內(nèi)存塊)。
var s0 []int func g(s1 []int) { // Assume the length of s1 is much larger than 30. s0 = s1[len(s1)-30:] }
如果我們想避免這種內(nèi)存泄漏,我們必須復(fù)制 s0
的 30 個元素,這樣 s0
的活躍性就不會阻止收集承載 s1
元素的內(nèi)存塊。
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. }
未重置子切片指針導(dǎo)致的內(nèi)存泄漏
在下面的代碼中,調(diào)用 h
函數(shù)后,為切片 s
的第一個和最后一個元素分配的內(nèi)存塊將丟失。
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
值分配的兩個內(nèi)存塊。
如果我們想避免這種內(nèi)存泄漏,我們必須重置丟失元素中存儲的指針。
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導(dǎo)致的內(nèi)存泄漏
有時,Go 程序中的某些 goroutine 可能會永遠(yuǎn)處于阻塞狀態(tài)。這樣的 goroutine 被稱為掛起的 goroutine。Go 運(yùn)行時不會終止掛起的 goroutine,因此為掛起的 goroutine 分配的資源(以及它們引用的內(nèi)存塊)永遠(yuǎn)不會被垃圾回收。
Go 運(yùn)行時不會殺死掛起的 Goroutine 有兩個原因。一是 Go 運(yùn)行時有時很難判斷一個阻塞的 Goroutine 是否會被永久阻塞。二是我們有時會故意讓 Goroutine 掛起。例如,有時我們可能會讓 Go 程序的主 Goroutine 掛起,以避免程序退出。
如果不停止time.Ticker也會導(dǎo)致內(nèi)存泄漏
當(dāng) time.Timer
值不再使用時,它會在一段時間后被垃圾回收。但 time.Ticker
值則不然。我們應(yīng)該在 time.Ticker
值不再使用時停止它。
不正確地使用終結(jié)器會導(dǎo)致真正的內(nèi)存泄漏
為屬于循環(huán)引用組的成員值設(shè)置終結(jié)器(finalizer)可能會阻止為該循環(huán)引用組分配的所有內(nèi)存塊被回收。這是真正的內(nèi)存泄漏,不是某種假象。
例如,在調(diào)用并退出以下函數(shù)后,分配給 x
和 y
的內(nèi)存塊不能保證在未來的垃圾收集中被收集。
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)引用組中的值設(shè)置終結(jié)器。
延遲函數(shù)調(diào)用導(dǎo)致的某種資源泄漏
非常大的延遲調(diào)用堆棧也可能會消耗大量內(nèi)存,并且如果某些調(diào)用延遲太多,某些資源可能無法及時釋放。
例如,如果在調(diào)用以下函數(shù)時需要處理許多文件,那么在函數(shù)退出之前將有大量文件處理程序無法釋放。
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 }
對于這種情況,我們可以使用匿名函數(shù)來封裝延遲調(diào)用,以便延遲函數(shù)調(diào)用能夠更早地執(zhí)行。例如,上面的函數(shù)可以重寫并改進(jìn)為
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 }
當(dāng)然不要犯以下錯誤,需要有些同學(xué)將需要延時調(diào)用的函數(shù)字節(jié)省略,導(dǎo)致資源泄漏
_, err := os.Open(file.path)
如果是http請求,還會導(dǎo)致服務(wù)端擠壓大量的連接無法釋放
到此這篇關(guān)于go語言內(nèi)存泄漏的常見形式的文章就介紹到這了,更多相關(guān)go語言內(nèi)存泄漏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決goland中編輯tpl文件不高亮沒智能補(bǔ)全的問題
這篇文章主要介紹了解決goland中編輯tpl文件不高亮沒智能補(bǔ)全的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12Go語言中strings.HasPrefix、strings.Split、strings.SplitN()?函數(shù)
本文主要介紹了Go語言中strings.HasPrefix、strings.Split、strings.SplitN()函數(shù),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08golang框架中跨服務(wù)的最佳通信協(xié)議和工具
在 go 框架中實現(xiàn)跨服務(wù)通信的最佳實踐包括使用 grpc(適用于低延遲高吞吐量)、http 客戶端(適用于 restful api)和消息隊列(適用于異步解耦通信),在選擇通信方式時,應(yīng)考慮服務(wù)交互模式、性能要求和部署環(huán)境等因素2024-06-06解析golang 標(biāo)準(zhǔn)庫template的代碼生成方法
這個項目的自動生成代碼都是基于 golang 的標(biāo)準(zhǔn)庫 template 的,所以這篇文章也算是對使用 template 庫的一次總結(jié),本文通過實例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-11-11