W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
當(dāng)使用一門支持自動(dòng)垃圾回收的語言編程時(shí),一般來說我們不需要關(guān)心內(nèi)存泄露問題,因?yàn)槌绦虻倪\(yùn)行時(shí)會(huì)負(fù)責(zé)回收不再使用的內(nèi)存。 但是,我們確實(shí)也需要知道一些特殊的可能會(huì)造成暫時(shí)性或永久性內(nèi)存泄露的情形。 本文的余下部分將列出一些這樣的情形。
Go白皮書并沒有說明一個(gè)子字符串表達(dá)式的結(jié)果(子)字符串和基礎(chǔ)字符串是否應(yīng)該共享一個(gè)承載底層字節(jié)序列的內(nèi)存塊。 但標(biāo)準(zhǔn)編譯器確實(shí)讓它們共享一個(gè)內(nèi)存塊,而且很多標(biāo)準(zhǔn)庫包的函數(shù)原型設(shè)計(jì)也默認(rèn)了這一點(diǎn)。 這是一個(gè)好的設(shè)計(jì),它不僅節(jié)省內(nèi)存,而且還減少了CPU消耗。 但是有時(shí)候它會(huì)造成暫時(shí)性的內(nèi)存泄露。
比如,當(dāng)下面這段代碼中的demo
函數(shù)被調(diào)用之后,將會(huì)造成大約1M字節(jié)的暫時(shí)性內(nèi)存泄露,直到包級變量s0
的值在其它某處被重新修改為止。
var s0 string // 一個(gè)包級變量
// 一個(gè)演示目的函數(shù)。
func f(s1 string) {
s0 = s1[:50]
// 目前,s0和s1共享著承載它們的字節(jié)序列的同一個(gè)內(nèi)存塊。
// 雖然s1到這里已經(jīng)不再被使用了,但是s0仍然在使用中,
// 所以它們共享的內(nèi)存塊將不會(huì)被回收。雖然此內(nèi)存塊中
// 只有50字節(jié)被真正使用,而其它字節(jié)卻無法再被使用。
}
func demo() {
s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
f(s)
}
為防止上面的f
函數(shù)產(chǎn)生臨時(shí)性內(nèi)存泄露,我們可以將子字符串表達(dá)式的結(jié)果轉(zhuǎn)換為一個(gè)字節(jié)切片,然后再轉(zhuǎn)換回來。
func f(s1 string) {
s0 = string([]byte(s1[:50]))
}
此種防止臨時(shí)性內(nèi)存泄露的方法不是很高效,因?yàn)樵诖诉^程中底層的字節(jié)序列被復(fù)制了兩次,其中一次是不必要的。
我們可以利用官方Go標(biāo)準(zhǔn)編譯器對字符串銜接所做的優(yōu)化來防止一次不必要的復(fù)制,代價(jià)是有一個(gè)字節(jié)的浪費(fèi)。
func f(s1 string) {
s0 = (" " + s1[:50])[1:]
}
此第二種防止臨時(shí)性內(nèi)存泄露的方法有可能在將來會(huì)失效,并且它對于其它編譯器來說很可能是無效的。
第三種防止臨時(shí)性內(nèi)存泄露的方法是使用在Go 1.10種引入的strings.Builder
類型來防止一次不必要的復(fù)制。
import "strings"
func f(s1 string) {
var b strings.Builder
b.Grow(50)
b.WriteString(s1[:50])
s0 = b.String()
}
此第三種方法的缺點(diǎn)是它的實(shí)現(xiàn)有些啰嗦(和前兩種方法相比)。 一個(gè)好消息是,從Go 1.12開始,我們可以調(diào)用strings
標(biāo)準(zhǔn)庫包中的Repeat
函數(shù)來克隆一個(gè)字符串。 從Go 1.12開始,此函數(shù)將利用strings.Builder
來防止一次不必要的復(fù)制。
從Go 1.18開始,strings
標(biāo)準(zhǔn)庫包中引入了一個(gè)Clone
函數(shù)。 調(diào)用此函數(shù)為克隆一個(gè)字符串的最佳實(shí)現(xiàn)方式。
和子字符串情形類似,子切片也可能會(huì)造成暫時(shí)性的內(nèi)存泄露。 在下面這段代碼中,當(dāng)函數(shù)g
被調(diào)用之后,承載著切片s1
的元素的內(nèi)存塊的開頭大段內(nèi)存將不再可用(假設(shè)沒有其它值引用著此內(nèi)存塊)。 同時(shí)因?yàn)?code>s0仍在引用著此內(nèi)存塊,所以此內(nèi)存塊得不到釋放。
var s0 []int
func g(s1 []int) {
// 假設(shè)s1的長度遠(yuǎn)大于30。
s0 = s1[len(s1)-30:]
}
如果我們想防止這樣的臨時(shí)性內(nèi)存泄露,我們必須在函數(shù)g
中將30個(gè)元素均復(fù)制一份,使得切片s0
和s1
不共享承載底層元素的內(nèi)存塊。
func g(s1 []int) {
s0 = make([]int, 30)
copy(s0, s1[len(s1)-30:])
// 現(xiàn)在,如果再?zèng)]有其它值引用著承載著s1元素的內(nèi)存塊,
// 則此內(nèi)存塊可以被回收了。
}
在下面這段代碼中,h
函數(shù)調(diào)用之后,s
的首尾兩個(gè)元素將不再可用。
func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// 使用此s切片 ...
return s[1:3:3]
}
只要h
函數(shù)調(diào)用返回的切片仍在被使用中,它的各個(gè)元素就不會(huì)回收,包括首尾兩個(gè)已經(jīng)丟失的元素。 因此這兩個(gè)已經(jīng)丟失的元素引用著的兩個(gè)int
值也不會(huì)被回收,即使我們再也無法使用這兩個(gè)int
值。
為了防止這樣的暫時(shí)性內(nèi)存泄露,我們必須重置丟失的元素中的指針。
func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// 使用此s切片 ...
s[0], s[len(s)-1] = nil, nil // 重置首尾元素指針
return s[1:3:3]
}
我們經(jīng)常需要在刪除切片元素操作中重置一些切片元素中的指針值。
有時(shí),一個(gè)程序中的某些協(xié)程會(huì)永久處于阻塞狀態(tài)。 Go運(yùn)行時(shí)并不會(huì)將處于永久阻塞狀態(tài)的協(xié)程殺掉,因此永久處于阻塞狀態(tài)的協(xié)程所占用的資源將永得不到釋放。
Go運(yùn)行時(shí)出于兩個(gè)原因并不殺掉處于永久阻塞狀態(tài)的協(xié)程。 一是有時(shí)候Go運(yùn)行時(shí)很難分辨出一個(gè)處于阻塞狀態(tài)的協(xié)程是永久阻塞還是暫時(shí)性阻塞;二是有時(shí)我們可能故意永久阻塞某些協(xié)程。
我們應(yīng)該避免因?yàn)榇a設(shè)計(jì)中的一些錯(cuò)誤而導(dǎo)致一些協(xié)程處于永久阻塞狀態(tài)。
當(dāng)一個(gè)time.Timer
值不再被使用,一段時(shí)間后它將被自動(dòng)垃圾回收掉。 但對于一個(gè)不再使用的time.Ticker
值,我們必須調(diào)用它的Stop
方法結(jié)束它,否則它將永遠(yuǎn)不會(huì)得到回收。
將一個(gè)終結(jié)器設(shè)置到一個(gè)循環(huán)引用值組中的一個(gè)值上可能導(dǎo)致被此值組中的值所引用的內(nèi)存塊永遠(yuǎn)得不到回收。
比如,當(dāng)下面這個(gè)函數(shù)被調(diào)用后,承載著x
和y
的兩個(gè)內(nèi)存塊將不保證會(huì)被逐漸回收。
func memoryLeaking() {
type T struct {
v [1<<20]int
t *T
}
var finalizer = func(t *T) {
fmt.Println("finalizer called")
}
var x, y T
// 此SetFinalizer函數(shù)調(diào)用將使x逃逸到堆上。
runtime.SetFinalizer(&x, finalizer)
// 下面這行將形成一個(gè)包含x和y的循環(huán)引用值組。
// 這有可能造成x和y不可回收。
x.t, y.t = &y, &x // y也逃逸到了堆上。
}
所以,不要為一個(gè)循環(huán)引用值組中的值設(shè)置終結(jié)器。
順便說一下,我們不應(yīng)該把終結(jié)器用做析構(gòu)函數(shù)。
請閱讀此文以獲得詳情。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: