Go 切片的一種有趣內(nèi)存泄漏方式
今天我在看 Prashant Varanasi 的 Go 發(fā)布會(huì)演講:使用火焰圖進(jìn)行生產(chǎn)分析[1](Analyzing production using Flamegraphs),在演講開(kāi)始的第 28 分鐘他提到了一種涉及切片的有趣且棘手的內(nèi)存泄漏。為了自我提升,我將在這里寫(xiě)一下該內(nèi)存泄漏的一種形式,并說(shuō)明它是如何發(fā)生的。
首先,對(duì)于像 Go 這樣的垃圾收集語(yǔ)言來(lái)說(shuō),內(nèi)存泄漏是保留了對(duì)對(duì)象的非預(yù)期引用所造成的。垃圾收集器會(huì)幫你尋找并釋放對(duì)象,但前提是它們事實(shí)上并沒(méi)有被使用。如果你保留了對(duì)它們的引用,它們會(huì)留下來(lái)。有時(shí)最終結(jié)果很簡(jiǎn)單(也行你故意保留一個(gè)較小的結(jié)構(gòu),但沒(méi)意識(shí)到它引用了一個(gè)較大的結(jié)構(gòu)),但有時(shí)候這種保留隱藏在某些東西的運(yùn)行時(shí)實(shí)現(xiàn)里。這改變了我們對(duì)切片的看法。
簡(jiǎn)化之后,Prashant 處理的代碼在一個(gè)切片中維護(hù)了當(dāng)前在使用的元素的集合。當(dāng)一個(gè)元素不再被使用時(shí),它被轉(zhuǎn)移到了切片的末尾,然后切片被截?cái)喽s小(保持不變的是切片只保留使用的元素)。然而,縮小切片并不會(huì)縮小其依賴的數(shù)組,用 Go 的術(shù)語(yǔ)來(lái)說(shuō),減小了切片的長(zhǎng)度但是并沒(méi)有減少容量。由于底層依賴的數(shù)組沒(méi)有變動(dòng),而該數(shù)組保留了一個(gè)理論上已經(jīng)被丟棄了的元素的引用,以及該元素所引用的所有其他對(duì)象。即使是代碼不可見(jiàn)的引用被保留,Go 垃圾收集器仍然會(huì)將該元素看做是還在使用中。代碼認(rèn)為以及被丟棄了的元素實(shí)際上并沒(méi)有被釋放,這就造成了內(nèi)存泄漏。
現(xiàn)在,我查看了 Go 運(yùn)行時(shí)和編譯器代碼,并對(duì)該問(wèn)題進(jìn)行了一些思考,我清楚地意識(shí)到了這是任何切片截?cái)嗟耐ㄓ脝?wèn)題。Go 絕不會(huì)嘗試縮小切片的底層數(shù)組,而且通常來(lái)說(shuō)這樣做是不可能的,因?yàn)?span style="font-weight: bold;color: #ff3502;">一個(gè)底層數(shù)組可能被多個(gè)切片[2]或其他引用所共享。這顯然會(huì)嚴(yán)重影響指向包含指針的對(duì)象的切片,但對(duì)于指向普通的舊數(shù)據(jù)的切片也可能很重要,尤其是當(dāng)它們比較大的時(shí)候(比如你有一個(gè) Point 的切片,每個(gè) Point 有三個(gè)浮點(diǎn)數(shù))。
對(duì)于包含指針或者包含持有著指針的結(jié)構(gòu)的切片來(lái)說(shuō),明顯的修復(fù)方式(這是Uber 代碼中采用的修復(fù)方式[3])是在截?cái)嗲衅皩⒛┪驳闹羔樦脼榭铡_@樣保留了完整的底層數(shù)組,但拋棄了對(duì)其他內(nèi)存的引用,而這些其他的內(nèi)存是真正內(nèi)存泄漏的地方。
對(duì)于實(shí)際的底層數(shù)組可能會(huì)有大量?jī)?nèi)存消耗的切片來(lái)說(shuō),我想到可能有兩種做法,一種特殊,一種通用。特殊的一種是檢查代碼中“大小截?cái)酁榱恪钡那闆r,并專(zhuān)門(mén)將切片本身置為空,而不是僅僅使用標(biāo)準(zhǔn)的切片截?cái)喙δ軄?lái)截?cái)唷Mㄓ玫淖龇ㄊ敲鞔_地強(qiáng)制使用切片拷貝而不是僅僅截?cái)啵ň腿?span style="font-weight: bold;color: #ff3502;">我對(duì)切片可變性的評(píng)論[4]提到的)。強(qiáng)制使用拷貝所帶來(lái)的缺點(diǎn)是,某些時(shí)候可能會(huì)帶來(lái)更大的開(kāi)銷(xiāo)。你可以通過(guò)僅在切片的容量遠(yuǎn)遠(yuǎn)超出新切片的長(zhǎng)度的時(shí)候才強(qiáng)制使用拷貝的方式來(lái)進(jìn)行優(yōu)化。
補(bǔ)充:(對(duì)垃圾收集而言)三索引的切片截?cái)嗍俏kU(xiǎn)的
Go 切片表達(dá)式[5]允許在起終點(diǎn)之外,使用很少使用的第三個(gè)索引來(lái)設(shè)置新切片的容量。你也許會(huì)想到采用這種形式限制切片,來(lái)作為解決垃圾收集問(wèn)題的辦法:
slc?=?slc[:newlen:newlen]
不幸的是,這樣并不會(huì)達(dá)到你想要的效果,而且會(huì)適得其反。設(shè)置新切片的容量完全不會(huì)改變底層的依賴數(shù)組,也不會(huì)讓 Go 分配一個(gè)新的內(nèi)存,但這卻意味著你無(wú)法獲取數(shù)組大小的信息(否則可以通過(guò)切片的容量來(lái)得到它)。這樣造成的唯一影響是強(qiáng)制隨后的 append() 重新分配新的底層數(shù)組。
via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoSlicesMemoryLeak
作者:ChrisSiebenmann[6]譯者:dust347[7]校對(duì):polaris1119[8]
本文由 GCTT[9] 原創(chuàng)編譯,Go 中文網(wǎng)[10] 榮譽(yù)推出
參考資料
使用火焰圖進(jìn)行生產(chǎn)分析: https://www.youtube.com/watch?v=aAhNDgEZj_U
[2]一個(gè)底層數(shù)組可能被多個(gè)切片: https://utcc.utoronto.ca/~cks/space/blog/programming/GoSliceMutability
[3]Uber 代碼中采用的修復(fù)方式: https://github.com/uber/tchannel-go/commit/63a486b96821eaa6fb2299663dda5c529cc04666#diff-32e1ab53c69bf3272bd9e4b51b9bb105
[4]我對(duì)切片可變性的評(píng)論: https://utcc.utoronto.ca/~cks/space/blog/programming/GoSliceMutability
[5]Go 切片表達(dá)式: https://golang.org/ref/spec#Slice_expressions
[6]ChrisSiebenmann: https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann
[7]dust347: https://github.com/dust347
[8]polaris1119: https://github.com/polaris1119
[9]GCTT: https://github.com/studygolang/GCTT
[10]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀

