新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「腦子進(jìn)煎魚了」,作者陳煎魚。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚了公眾號(hào)。

我們提供的服務(wù)有:網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作、微信公眾號(hào)開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、哈巴河ssl等。為超過千家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的哈巴河網(wǎng)站制作公司
大家好,我是煎魚。
前段時(shí)間在我的 Go 讀者群里,有小伙伴們?cè)诩m結(jié)切片(slice)的問題,我寫了這篇《Go 切片這道題,吵了一個(gè)下午!》,引起了一撥各種討論,還是比較欣慰的。
這不,有小伙伴給我提出了新的題材:
來自讀者微信提問
提出的是 Go 中很容易踩坑的切片內(nèi)存泄露問題。作為寵粉的煎魚肯定不會(huì)放過,爭(zhēng)取讓大家都避開這個(gè) “坑”。
今天這篇文章,就由煎魚帶大家來了解這個(gè)問題:Go 切片可能可以怎么泄露法?
切片泄露的可能
在業(yè)務(wù)代碼的編寫上,我們經(jīng)常會(huì)接受來自外部的接口數(shù)據(jù),再把他插入到對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)中去,再進(jìn)行下一步的業(yè)務(wù)聚合、裁剪、封裝、處理。
像在 PHP 語言,常常會(huì)放到數(shù)組(array)中。在 Go 語言,會(huì)放到切片(slice)中。因此在 Go 的切片處理邏輯中,常常會(huì)涉及到如下類似的動(dòng)作。
示例代碼如下:
- var a []int
- func f(b []int) []int {
- a = b[:2]
- return a
- }
- func main() {
- ...
- }
仔細(xì)想想,這段程序有沒有問題,是否存在內(nèi)存泄露的風(fēng)險(xiǎn)?
答案是:有的。有明確的切片內(nèi)存泄露的可能性和風(fēng)險(xiǎn)。
切片底層結(jié)構(gòu)
可能有些小伙伴會(huì)疑惑,怎么就有問題了,是哪里有問題?
這里就得復(fù)習(xí)一下切片的底層基本數(shù)據(jù)結(jié)構(gòu)了,切片在運(yùn)行時(shí)的表現(xiàn)是 SliceHeader 結(jié)構(gòu)體,定義如下:
- type SliceHeader struct {
- Data uintptr
- Len int
- Cap int
- }
- Data:指向具體的底層數(shù)組。
- Len:代表切片的長度。
- Cap:代表切片的容量。
要點(diǎn)是:切片真正存儲(chǔ)數(shù)據(jù)的地方,是一個(gè)數(shù)組。切片的 Data 屬性中存儲(chǔ)的是指向所引用的數(shù)組指針地址。
背后的原因
在上述案例中,我們有一個(gè)包全局變量 a,共有 2 個(gè)切片 a 和 b,截取了 b 的一部分賦值給了 a,兩者存在著關(guān)聯(lián)。
從程序的直面來看,截取了 b 的一部分賦值給了 a,結(jié)構(gòu)似乎是如下圖:
但我們進(jìn)一步打開程序底層來看,他應(yīng)該是如下圖所示:
切片 a 和 b 都共享著同一個(gè)底層數(shù)組(共享內(nèi)存塊),sliceB 包含全部所引用的字符。sliceA 只包含了 [:2],也就是 0 和 1 兩個(gè)索引位的字符。
那他們泄露在哪里了?
泄露的點(diǎn)
泄露的點(diǎn),就在于雖然切片 b 已經(jīng)在函數(shù)內(nèi)結(jié)束了他的使命了,不再使用了。但切片 a 還在使用,切片 a 和 切片 b 引用的是同一塊底層數(shù)組(共享內(nèi)存塊)。
關(guān)鍵點(diǎn):切片 a 引用了底層數(shù)組中的一段。
雖然切片 a 只有底層數(shù)組中 0 和 1 兩個(gè)索引位正在被使用,其余未使用的底層數(shù)組空間毫無作用。但由于正在被引用,他們也不會(huì)被 GC,因此造成了泄露。
解決辦法
解決的辦法,就是利用切片的特性。當(dāng)切片的容量空間不足時(shí),會(huì)重新申請(qǐng)一個(gè)新的底層數(shù)組來存儲(chǔ),讓兩者徹底分手。
示例代碼如下:
- var a []int
- var c []int // 第三者
- func f(b []int) []int {
- a = b[:2]
- // 新的切片 append 導(dǎo)致切片擴(kuò)容
- c = append(c, b[:2]...)
- fmt.Printf("a: %p\nc: %p\nb: %p\n", &a[0], &c[0], &b[0])
- return a
- }
輸出結(jié)果:
- a: 0xc000102060
- c: 0xc000124010
- b: 0xc000102060
這段程序,新增了一個(gè)變量 c,他容量為 0。此時(shí)將期望的數(shù)據(jù),追加過去。自然而然他就會(huì)遇到容量空間不足的情況,也就能實(shí)現(xiàn)申請(qǐng)新底層數(shù)據(jù)。
我們?cè)賹⒃镜那衅脼?nil,就能成功實(shí)現(xiàn)兩者分手的目標(biāo)了。
總結(jié)
在今天這篇文章中,我們介紹了 Go 切片的一種常見的內(nèi)存泄露方式。雖然我們?cè)谌粘J褂玫臅r(shí)候可能沒關(guān)注到。
主要原因還是由于切片的大多數(shù)使用場(chǎng)景,體量都比較小。又或是不知不覺就自己擴(kuò)容了,就變成暫時(shí)性泄露了。
這依然是存在風(fēng)險(xiǎn)的,在編寫 Go 代碼時(shí)需要謹(jǐn)慎。畢竟這可是 Go 語言官方自己都踩過坑的 “坑”。
參考
An interesting way to leak memory with Go slices
internal/poll: avoid memory leak in Writev
slice 類型內(nèi)存泄露的邏輯
golang slice內(nèi)存泄露回收
新聞標(biāo)題:Go切片導(dǎo)致內(nèi)存泄露,被坑兩次了!
文章分享:http://www.5511xx.com/article/cojhgec.html


咨詢
建站咨詢
