Go GC 棧對象與棧跟蹤

GC掃描棧

問題的關(guān)鍵在于這段代碼:

t?:=?T{...}
p?:=?&t
for?{
???if?…?{?p?=?…?}
}

編譯器決定在棧上分配 T鳄炉,并且因?yàn)榫幾g器無法跟蹤其地址結(jié)束的位置杜耙,所以編譯器保守地決定 t 始終是存活的。

但是在 for 循環(huán)中拂盯,當(dāng) p 被賦值時(shí)泥技,t 變成死亡的。
所以在 p 被重新分配之后磕仅,t 中的任何指針都可能保持一個(gè)它不應(yīng)該保持的對象存活。

我們將在棧上調(diào)用其地址被 “棧對象” 占用的變量簸呈,目標(biāo)是僅在棧對象中的數(shù)據(jù)真實(shí)存活時(shí)掃描棧對象榕订,即如果對象再次直接引用(如,t.x)或間接引用(如蜕便,p.x)劫恒。

我們開始像往常一樣掃描一個(gè)棧,從最低(最新)的棧幀開始轿腺,一直向上两嘴。
我們使用指針位圖來查找每個(gè)棧幀中的所有存活的指針。
通常族壳,棧對象的指針不會標(biāo)記為存活(但請參見下文)憔辫。
所有指向堆的指針都像往常一樣被標(biāo)記,任何指向棧的指針都被忽略(目前)仿荆。
對于遇到的每一幀贰您,我們查看該幀是否有棧對象。
對于該幀中的每個(gè)棧對象拢操,我們將其添加到棧對象列表中锦亦。

如果我們到達(dá)棧頂部并找不到棧對象,那么就結(jié)束了令境。

如果我們確實(shí)找到了一些棧對象杠园,我們必須再次掃描棧。

首先舔庶,我們將棧對象組織成一個(gè)數(shù)據(jù)結(jié)構(gòu)抛蚁,該結(jié)構(gòu)提供按地址快速查找的功能陈醒,可能是一個(gè)二叉樹。由于我們可以按地址順序枚舉棧對象篮绿,所以應(yīng)該很容易使用它們的地址作為 key 來生成二叉樹(在 O(n) 時(shí)間內(nèi))孵延。

然后我們再次掃描棧,只查找指向棧的指針亲配。
對于任何這樣的指針尘应,我們在二叉樹中查找它們,看看它們是否指向任何棧對象吼虎。
如果是犬钢,我們將棧對象標(biāo)記為存活(如果我們還沒有這樣做),并將其放入正在掃描的工作列表中思灰。當(dāng)我們完成棧掃描時(shí)玷犹,我們現(xiàn)在在工作列表中有了棧對象的根集(root set)。
然后洒疚,我們不斷從工作列表中彈出一個(gè)對象并掃描它(可能將堆對象或其他棧對象標(biāo)記為存活)歹颓,直到工作列表為空。

我們可以為每個(gè)棧對象分配相鄰的空間來使用這個(gè)算法油湖。我們稱之為棧對象“header”巍扛。
header 可能包含二叉樹的左右指針,一個(gè)標(biāo)記位和用于單鏈表工作隊(duì)列的指針乏德。
header 有一些空間開銷撤奸,但是它應(yīng)該很小,并且可以自由分配喊括,因?yàn)樗皇鞘箺笠稽c(diǎn)胧瓜。
header 只需要在第一次掃描是找到棧對象時(shí)才初始化。因此我們可以在正常執(zhí)行期間將垃圾留在那里郑什。

記錄棧對象

我們在funcdata中為函數(shù)保留了一個(gè)額外的映射府喳,該函數(shù)包含幀中每個(gè)棧對象的幀偏移,大小和指針映射的列表蹦误。

請注意劫拢,只需要記錄具有指針的棧對象。

_FUNCDATA_StackObjects?=?4

堆棧對象相對較少强胰,因此棧對象數(shù)據(jù)的編碼不是很關(guān)鍵舱沧。
我們可以像這樣編碼棧對象funcdata

(偏移大小(isptr)*)*

其中偶洋,每個(gè)對象在幀中編碼它的起始偏移量熟吏、它的大小以及該對象中每個(gè)指針 slotsize/ptrSize 0/1項(xiàng)。

含糊不清的存活對象(ambiguously live objects)

有了這種新機(jī)制,我們就可以在當(dāng)前編譯器中為含糊不清的存活對象去掉所有代碼牵寺。
我們只需要在存活的 ptr 位圖中標(biāo)記那些通過直接訪問(而不是通過指針)存活的指針悍引。
我們的新機(jī)制包含以前用于跟蹤棧對象的邏輯,該邏輯是:存活只是因?yàn)橹赶蛩鼈兊闹羔樋赡苁谴婊畹摹?/p>

其他說明

對象可以作為棧對象被列出帽氓,并在存活的 ptr 位圖中被標(biāo)記趣斤。
例如:

t?:=?T{}
p?:=?&t
f()
println(t.a)
g()
println(p.b)

在調(diào)用 f 時(shí),t 中的指針將在 ptr 位圖中標(biāo)記為存活的(可能只標(biāo)記 ta 字段)黎休。在調(diào)用 g 時(shí)不會浓领。

清除

清掃器(sweeper)由兩種不同的算法組成:

  • 對象回收器查找和釋放 span 中未標(biāo)記的 slots
    如果沒有標(biāo)記任何對象势腮,它可以釋放整個(gè) span联贩,但這不是它的目標(biāo)。
    可以通過 mcentral.cacheSpan(for mcentral spans)同步驅(qū)動捎拯,也可以通過 mheap_.sweepSpans 中所有正在使用的 spans 列表中的 sweepone 異步驅(qū)動泪幌。

  • span 回收器查找不包含標(biāo)記對象的 span,并釋放整個(gè) span署照。
    這是一個(gè)單獨(dú)的算法祸泪,因?yàn)獒尫耪麄€(gè) spans 對于對象回收器來說是最困難的任務(wù),但是在分配新 spans 時(shí)非常關(guān)鍵建芙。
    這個(gè)的入口點(diǎn)是 mheap_.reclaim浴滴,它是由堆區(qū)域中的頁面標(biāo)記位圖的順序掃描驅(qū)動的。

兩種算法最終都調(diào)用 mspan.sweep岁钓,它掃描單個(gè)堆 span

棧對象和棧跟蹤

堆跟蹤解決了確定棧的哪些部分是存活的并且應(yīng)該被掃描的問題微王。它作為掃描單個(gè) goroutine 棧的一部分運(yùn)行屡限。

通常,靜態(tài)地確定堆棧的哪些部分是存活的很容易炕倘,因?yàn)橛脩舸a對棧對象有顯式的引用(讀和寫)钧大。
編譯器可以進(jìn)行簡單的數(shù)據(jù)流分析,以確定代碼中每個(gè)點(diǎn)的棧變量的活躍性罩旋。
有關(guān)該分析啊央,請參閱:$GOROOT/src/cmd/compile/internal/gc/plive.go

但是,當(dāng)我們獲取堆棧變量的地址時(shí)涨醋,確定該變量是否仍然存活就不太清楚了瓜饥。
我們?nèi)匀豢梢圆檎异o態(tài)訪問,但是通過指向變量的指針進(jìn)行的訪問通常很難靜態(tài)跟蹤浴骂。
該指針可以在棧上的函數(shù)之間傳遞乓土,有條件地保留等。

相反,我們將動態(tài)跟蹤指向棧變量的指針趣苏。
所有指向棧分配的變量的指針本身都將位于棧的某個(gè)位置(或者在相關(guān)位置狡相,如 defer),因此我們可以有效地找到它們食磕。

棧跟蹤被組織為迷你的垃圾收集跟蹤通道尽棕。這個(gè)垃圾收集中的對象是棧上的所有變量,這些變量的地址已被占用彬伦,它們本身包含一個(gè)指針滔悉。我們稱這些變量為 “棧對象”

我們首先確定堆棧上的所有棧對象和可能指向棧的所有靜態(tài)存活指針媚朦。然后我們處理每個(gè)指針氧敢,看它是否指向棧對象。如果是询张,我們掃描該棧對象孙乖。棧對象可能包含指向堆的指針,在這種情況下份氧,這些指針將被傳遞給 主 GC唯袄。棧對象也可能包含指向棧的指針,在這種情況下蜗帜,我們將它們添加到棧指針集中恋拷。

一旦我們處理完所有指針(包括我們在處理過程中添加的指針),我們就找到了所有存活的棧對象厅缺。
任何死亡的棧對象都不會被掃描蔬顾,并且它們的內(nèi)容也不會保持堆對象存活。與 主 GC 不同湘捎,我們無法清除死亡的棧對象诀豁;它們一直處于垂死狀態(tài),直到包含它們的棧幀被彈出窥妇。

椣鲜ぃ可能如下所示:

?+----------+
?|?foo()????|
?|?+------+?|
?|?|??A???|?|?<---\
?|?+------+?|?????|
?|??????????|?????|
?|?+------+?|?????|
?|?|??B???|?|?????|
?|?+------+?|?????|
?|??????????|?????|
?+----------+?????|
?|?bar()????|?????|
?|?+------+?|?????|
?|?|??C???|?|?<-\?|
?|?+----|-+?|???|?|
?|??????|???|???|?|
?|?+----v-+?|???|?|
?|?|??D??---------/
?|?+------+?|???|
?|??????????|???|
?+----------+???|
?|?baz()????|???|
?|?+------+?|???|
?|?|??E??-------/
?|?+------+?|
?|??????^???|
?|?F:?--/???|
?|??????????|
?+----------+

foo() 調(diào)用 bar() 調(diào)用 baz()。每個(gè)方法在棧上都有一個(gè)楨活翩。

foo() 有棧對象 AB烹骨。

bar() 有棧對象 CD, C 指向 D, D 指向 A

baz() 有一個(gè)棧對象 E 指向 C材泄,一個(gè)局部變量 F 指向 E沮焕。

從局部變量 F 中的指針開始,我們最終將掃描所有 E拉宗、C遇汞、DA (按照這個(gè)順序)。

B 不會被掃描,因?yàn)闆]有指向它的存活指針空入。

如果 B 也是靜態(tài)死的(這意味著 foo() 在調(diào)用 bar() 之后再也不會訪問 B)络它,那么 B 指向堆的指針就不被認(rèn)為是存活的。

tips:棧對象與棧掃描

//?stackObject?表示棧上的一個(gè)變量歪赢,該變量的地址已被占用化戳。
//
//go:notinheap
type?stackObject?struct?{
????off???uint32???????//?stack.lo?之上的偏移
????size??uint32???????//?對象的大小
????typ???*_type???????//?類型信息(對于?ptr/nonptr?位),如果對象已被掃描埋凯,則為?nil
????left??*stackObject?//?低地址對象
????right?*stackObject?//?高地址對象
}

//?如上可以看出棧對象是一個(gè)雙向鏈表
//?參見:go1.12beta2/src/runtime/mgcstack.go


//?scanstack?掃描?goroutine?的棧点楼,標(biāo)灰所有在該棧上找到的指針。
//
//?scanstack?被標(biāo)記為?go:systemstack白对,因?yàn)樗谑褂?workbuf?時(shí)不能被搶占掠廓。
//
//go:nowritebarrier
//go:systemstack
func?scanstack(gp?*g,?gcw?*gcWork)?{
......
????var?state?stackScanState
????state.stack?=?gp.stack
......
????//?查找并且掃描所有可達(dá)的棧對象。
????//?buildIndex?將?state.root?初始化為二叉搜索樹甩恼。
????//?應(yīng)該在所有?addObject?調(diào)用之后但在調(diào)用?findObject?之前調(diào)用它蟀瞧。
????//?使用棧對象數(shù)組構(gòu)造了一顆二叉查找樹。
????state.buildIndex()
????for?{

????????//?刪除并返回指向堆棧對象的潛在指針条摸。
????????//?如果沒有更多可用指針悦污,則返回0。
????????p?:=?state.getPtr()
????????if?p?==?0?{
????????????break
?????????}

????????//?findObject?返回包含地址?a?的堆棧對象(如果有)
????????obj?:=?state.findObject(p)
????????if?obj?==?nil?{
????????????continue
?????????}
......
?????}
......
}

//?參見:go1.12beta2/src/runtime/mgcmark.go

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钉蒲,一起剝皮案震驚了整個(gè)濱河市切端,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌顷啼,老刑警劉巖踏枣,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異钙蒙,居然都是意外死亡椰于,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門仪搔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蜻牢,你說我怎么就攤上這事烤咧。” “怎么了抢呆?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵煮嫌,是天一觀的道長。 經(jīng)常有香客問我抱虐,道長昌阿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮懦冰,結(jié)果婚禮上灶轰,老公的妹妹穿的比我還像新娘。我一直安慰自己刷钢,他們只是感情好笋颤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著内地,像睡著了一般伴澄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上阱缓,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天非凌,我揣著相機(jī)與錄音,去河邊找鬼荆针。 笑死敞嗡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的祭犯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼沃粗,長吁一口氣:“原來是場噩夢啊……” “哼粥惧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起最盅,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤突雪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后涡贱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咏删,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年问词,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了督函。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡激挪,死狀恐怖辰狡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垄分,我是刑警寧澤宛篇,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站薄湿,受9級特大地震影響叫倍,放射性物質(zhì)發(fā)生泄漏偷卧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一吆倦、第九天 我趴在偏房一處隱蔽的房頂上張望听诸。 院中可真熱鬧,春花似錦逼庞、人聲如沸蛇更。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽派任。三九已至,卻和暖如春璧南,著一層夾襖步出監(jiān)牢的瞬間掌逛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工司倚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留豆混,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓动知,卻偏偏與公主長得像皿伺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子盒粮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,101評論 1 32
  • 從三月份找實(shí)習(xí)到現(xiàn)在鸵鸥,面了一些公司,掛了不少丹皱,但最終還是拿到小米妒穴、百度、阿里摊崭、京東讼油、新浪、CVTE呢簸、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,246評論 11 349
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理矮台,因此不免有一些不準(zhǔn)確的地方,同時(shí)不同JDK版本的...
    高廣超閱讀 15,603評論 3 83
  • 第二部分 自動內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運(yùn)行數(shù)據(jù)區(qū)域 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,164評論 0 2
  • Java8張圖 11根时、字符串不變性 12瘦赫、equals()方法、hashCode()方法的區(qū)別 13啸箫、...
    Miley_MOJIE閱讀 3,707評論 0 11