日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

CoreCLR源码探索(四) GC内存收集器的内部实现 分析篇

發(fā)布時(shí)間:2023/12/4 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 CoreCLR源码探索(四) GC内存收集器的内部实现 分析篇 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

在這篇中我將講述GC Collector內(nèi)部的實(shí)現(xiàn), 這是CoreCLR中除了JIT以外最復(fù)雜部分,下面一些概念目前尚未有公開的文檔和書籍講到。

為了分析這部分我花了一個(gè)多月的時(shí)間,期間也多次向CoreCLR的開發(fā)組提問過,我有信心以下內(nèi)容都是比較準(zhǔn)確的,但如果你發(fā)現(xiàn)了錯(cuò)誤或者有疑問的地方請指出來,
以下的內(nèi)容基于CoreCLR 1.1.0的源代碼分析,以后可能會有所改變。

因?yàn)閮?nèi)容過長,我分成了兩篇,這一篇分析代碼,下一篇實(shí)際使用LLDB跟蹤GC收集垃圾的處理。

需要的預(yù)備知識

  • 看過BOTR中GC設(shè)計(jì)的文檔 原文 譯文

  • 看過我之前的系列文章, 碰到不明白的可以先跳過但最少需要看一遍

    • CoreCLR源碼探索(一) Object是什么

    • CoreCLR源碼探索(二) new是什么

    • CoreCLR源碼探索(三) GC內(nèi)存分配器的內(nèi)部實(shí)現(xiàn)

  • 對c中的指針有一定了解

  • 對常用數(shù)據(jù)結(jié)構(gòu)有一定了解, 例如鏈表

  • 對基礎(chǔ)c++語法有一定了解, 高級語法和STL不需要因?yàn)槲④浿挥昧说图壵Z法

GC的觸發(fā)

GC一般在已預(yù)留的內(nèi)存不夠用或者已經(jīng)分配量超過閾值時(shí)觸發(fā),場景包括:

不能給分配上下文指定新的空間時(shí)

當(dāng)調(diào)用try_allocate_more_space不能從segment結(jié)尾自由對象列表獲取新的空間時(shí)會觸發(fā)GC, 詳細(xì)可以看我上一篇中分析的代碼。

分配的數(shù)據(jù)量達(dá)到一定閾值時(shí)

閾值儲存在各個(gè)heap的dd_min_gc_size(初始值), dd_desired_allocation(動態(tài)調(diào)整值), dd_new_allocation(消耗值)中,每次給分配上下文指定空間時(shí)會減少dd_new_allocation。

如果dd_new_allocation變?yōu)樨?fù)數(shù)或者與dd_desired_allocation的比例小于一定值則觸發(fā)GC,
觸發(fā)完GC以后會重新調(diào)整dd_new_allocation到dd_desired_allocation。

參考new_allocation_limit, new_allocation_allowed和check_for_full_gc函數(shù)。

值得一提的是可以在.Net程序中使用GC.RegisterForFullGCNotification可以設(shè)置觸發(fā)GC需要的dd_new_allocation / dd_desired_allocation的比例(會儲存在fgn_maxgen_percent和fgn_loh_percent中), 設(shè)置一個(gè)大于0的比例可以讓GC觸發(fā)的更加頻繁。

StressGC

允許手動設(shè)置特殊的GC觸發(fā)策略, 參考這個(gè)文檔

作為例子,你可以試著在運(yùn)行程序前運(yùn)行export COMPlus_GCStress=1

GCStrees會通過調(diào)用GCStress<gc_on_alloc>::MaybeTrigger(acontext)觸發(fā),
如果你設(shè)置了COMPlus_GCStressStart環(huán)境變量,在調(diào)用MaybeTrigger一定次數(shù)后會強(qiáng)制觸發(fā)GC,另外還有COMPlus_GCStressStartAtJit等參數(shù),請參考上面的文檔。

默認(rèn)StressGC不會啟用。

手動觸發(fā)GC

在.Net程序中使用GC.Collect可以觸發(fā)手動觸發(fā)GC,我相信你們都知道。

調(diào)用.Net中的GC.Collect會調(diào)用CoreCLR中的GCHeap::GarbageCollect => GarbageCollectTry => GarbageCollectGeneration。

GC的處理

以下函數(shù)大部分都在gc.cpp里,在這個(gè)文件里的函數(shù)我就不一一標(biāo)出文件了。

GC的入口點(diǎn)

GC的入口點(diǎn)是GCHeap::GarbageCollectGeneration函數(shù),這個(gè)函數(shù)的主要作用是停止運(yùn)行引擎和調(diào)用各個(gè)gc_heap的gc_heap::garbage_collect函數(shù)

因?yàn)檫@一篇重點(diǎn)在于GC做出的處理,我將不對如何停止運(yùn)行引擎和后臺GC做出詳細(xì)的解釋,希望以后可以再寫一篇文章講述

以下是gc_heap::garbage_collect函數(shù),這個(gè)函數(shù)也是GC的入口點(diǎn)函數(shù),

主要作用是針對gc_heap做gc開始前和結(jié)束后的清理工作,例如重設(shè)各個(gè)線程的分配上下文和修改gc參數(shù)


GC的主函數(shù)

GC的主函數(shù)是gc1,包含了GC中最關(guān)鍵的處理,也是這一篇中需要重點(diǎn)講解的部分。

gc1中的總體流程在BOTR文檔已經(jīng)有初步的介紹:

首先是mark phase,標(biāo)記存活的對象

然后是plan phase,決定要壓縮還是要清掃

如果要壓縮則進(jìn)入relocate phase和compact phase

如果要清掃則進(jìn)入sweep phase

在看具體的代碼之前讓我們一起復(fù)習(xí)之前講到的Object的結(jié)構(gòu)


GC使用其中的2個(gè)bit來保存標(biāo)記(marked)和固定(pinned)

標(biāo)記(marked)表示對象是存活的,不應(yīng)該被收集,儲存在MethodTable指針 & 1中

固定(pinned)表示對象不能被移動(壓縮時(shí)不要移動這個(gè)對象), 儲存在對象頭 & 0x20000000中

這兩個(gè)bit會在mark_phase中被標(biāo)記,在plan_phase中被清除,不會殘留到GC結(jié)束后

再復(fù)習(xí)堆段(heap segment)的結(jié)構(gòu)


一個(gè)gc_heap中有兩個(gè)segment鏈表,一個(gè)是小對象(gen 0~gen 2)用的鏈表,一個(gè)是大對象(gen 3)用的鏈表,

其中鏈表的最后一個(gè)節(jié)點(diǎn)是ephemeral heap segment,只用來保存gen 0和gen 1的對象,各個(gè)代都有一個(gè)開始地址,在開始地址之后的對象屬于這個(gè)代或更年輕的代。


接下來我們將分別分析GC中的五個(gè)階段(mark_phase, plan_phase, relocate_phase, compact_phase, sweep_phase)的內(nèi)部處理

標(biāo)記階段(mark_phase)

這個(gè)階段的作用是找出收集垃圾的范圍(gc_low ~ gc_high)中有哪些對象是存活的,如果存活則標(biāo)記(m_pMethTab |= 1),

另外還會根據(jù)GC Handle查找有哪些對象是固定的(pinned),如果對象固定則標(biāo)記(m_uSyncBlockValue |= 0x20000000)。

簡單解釋下GC Handle和Pinned Object,GC Handle用于在托管代碼中調(diào)用非托管代碼時(shí)可以決定傳遞的指針的處理,

一個(gè)類型是Pinned的GC Handle可以防止GC在壓縮時(shí)移動對象,這樣非托管代碼中保存的指針地址不會失效,詳細(xì)可以看微軟的文檔。

在繼續(xù)看代碼之前我們先來了解Card Table的概念:

Card Table

如果你之前已經(jīng)了解過GC,可能知道有的語言實(shí)現(xiàn)GC會有一個(gè)根對象,從根對象一直掃描下去可以找到所有存活的對象。

但這樣有一個(gè)缺陷,如果對象很多,掃描的時(shí)間也會相應(yīng)的變長,為了提高效率,CoreCLR使用了分代GC(包括之前的.Net Framework都是分代GC),

分代GC可以只選擇掃描一部分的對象(年輕的對象更有可能被回收)而不是全部對象,那么分代GC的掃描是如何實(shí)現(xiàn)的?

在CoreCLR中對象之間的引用(例如B是A的成員或者B在數(shù)組A中,可以稱作A引用B)一般包含以下情況

各個(gè)線程棧(stack)和寄存器(register)中的對象引用堆段(heap segment)中的對象?

CoreCLR有辦法可以檢測到Managed Thread中在棧和寄存器中的對象

這些對象是根對象(GC Root)的一種

GC Handle表中的句柄引用堆段(heap segment)中的對象?

這些對象也是根對象的一種

析構(gòu)隊(duì)列中的對象引用堆段(heap segment)中的對象?

這些對象也是根對象的一種

同代對象之間的引用

隔代對象之間的引用


請考慮下圖的情況,我們這次只想掃描gen 0,棧中的對象A引用了gen 1的對象B,對象B引用了gen 0的對象C,

在掃描的時(shí)候因?yàn)锽不在掃描范圍(gc_low ~ gc_high)中,CoreCLR不會去繼續(xù)跟蹤B的引用,

如果這時(shí)候gen 0中無其他對象引用對象C,是否會導(dǎo)致對象C被誤回收?


為了解決這種情況導(dǎo)致的問題,CoreCLR使用了Card Table,所謂Card Table就是專門記錄跨代引用的一個(gè)數(shù)組


當(dāng)我們設(shè)置B.member = C的時(shí)候,JIT會把賦值替換為JIT_WriteBarrier(&B.member, C)(或同等的其他函數(shù))

JIT_WriteBarrier函數(shù)中會設(shè)置*dst = ref,并且如果ref在ephemeral heap segment中(ref可能是gen 0或gen 1的對象)時(shí),

設(shè)置dst在Card Table中所屬的字節(jié)為0xff,Card Table中一個(gè)字節(jié)默認(rèn)涵蓋的范圍在32位下是1024字節(jié),在64位下是2048字節(jié)。

需要注意的是這里的dst是B.member的地址而不是B的地址,B.member的地址會是B的地址加一定的偏移值,

而B自身的地址不一定會在Card Table中得到標(biāo)記,我們之后可以根據(jù)B.member的地址得到B的地址(可以看find_first_object函數(shù))。

有了Card Table以后,只回收年輕代(非Full GC)時(shí)除了掃描根對象以外我們還需要掃描Card Table中標(biāo)記的范圍來防止誤回收對象。


接下來我們看下GCHeap::Promote函數(shù),在plan_phase中掃描到的對象都會調(diào)用這個(gè)函數(shù)進(jìn)行標(biāo)記,

這個(gè)函數(shù)名稱雖然叫Promote但是里面只負(fù)責(zé)對對象進(jìn)行標(biāo)記,被標(biāo)記的對象不一定會升代


經(jīng)過標(biāo)記階段以后,在堆中存活的對象都被設(shè)置了marked標(biāo)記,如果對象是固定的還會被設(shè)置pinned標(biāo)記

接下來是計(jì)劃階段plan_phase:

計(jì)劃階段(plan_phase)

在這個(gè)階段首先會模擬壓縮和構(gòu)建Brick Table,在模擬完成后判斷是否應(yīng)該進(jìn)行實(shí)際的壓縮,

如果進(jìn)行實(shí)際的壓縮則進(jìn)入重定位階段(relocate_phase)和壓縮階段(compact_phase),否則進(jìn)入清掃階段(sweep_phase),

在繼續(xù)看代碼之前我們需要先了解計(jì)劃階段如何模擬壓縮和什么是Brick Table。

計(jì)劃階段如何模擬壓縮

計(jì)劃階段首先會根據(jù)相鄰的已標(biāo)記的對象創(chuàng)建plug,用于加快處理速度和減少需要的內(nèi)存空間,我們假定一段內(nèi)存中的對象如下圖


計(jì)劃階段會為這一段對象創(chuàng)建2個(gè)unpinned plug和一個(gè)pinned plug:


第一個(gè)plug是unpinned plug,包含了對象B, C,不固定地址

第二個(gè)plug是pinned plug,包含了對象E, F, G,固定地址

第三個(gè)plug是unpinned plug,包含了對象H,不固定地址

各個(gè)plug的信息保存在開始地址之前的一段內(nèi)存中,結(jié)構(gòu)如下

struct plug_and_gap

{

? ? // 在這個(gè)plug之前有多少空間是未被標(biāo)記(可回收)的

? ? ptrdiff_t ? gap;

? ? // 壓縮這個(gè)plug中的對象時(shí)需要移動的偏移值,一般是負(fù)數(shù)

? ? ptrdiff_t ? reloc;

? ? union

? ? {

? ? ? ? // 左邊節(jié)點(diǎn)和右邊節(jié)點(diǎn)

? ? ? ? pair ? ?m_pair;

? ? ? ? int ? ? lr; ?//for clearing the entire pair in one instruction

? ? };

? ? // 填充對象(防止覆蓋同步索引塊)

? ? plug ? ? ? ?m_plug;

};

眼尖的會發(fā)現(xiàn)上面的圖有兩個(gè)問題

對象G不是pinned但是也被歸到pinned plug里了?

這是因?yàn)閜inned plug會把下一個(gè)對象也拉進(jìn)來防止pinned object的末尾被覆蓋,具體請看下面的代碼

第三個(gè)plug把對象G的結(jié)尾給覆蓋(破壞)了?

對于這種情況原來的內(nèi)容會備份到saved_post_plug中,具體請看下面的代碼

多個(gè)plug會構(gòu)建成一棵樹,例如上面的三個(gè)plug會構(gòu)建成這樣的樹:

第一個(gè)plug: { gap: 24, reloc: 未定義, m_pair: { left: 0, right: 0 } }

第二個(gè)plug: { gap: 132, reloc: 0, m_pair: { left: -356, right: 206 } }

第三個(gè)plug: { gap: 24, reloc: 未定義, m_pair: { left: 0, right 0 } }

第二個(gè)plug的left和right保存的是離子節(jié)點(diǎn)plug的偏移值,

第三個(gè)plug的gap比較特殊,可能你們會覺得應(yīng)該是0但是會被設(shè)置為24(sizeof(gap_reloc_pair)),這個(gè)大小在實(shí)際復(fù)制第二個(gè)plug(compact_plug)的時(shí)候會加回來。

當(dāng)計(jì)劃階段找到一個(gè)plug的開始時(shí),

如果這個(gè)plug是pinned plug則加到mark_stack_array隊(duì)列中。

當(dāng)計(jì)劃階段找到一個(gè)plug的結(jié)尾時(shí),

如果這個(gè)plug是pinned plug則設(shè)置這個(gè)plug的大小并移動隊(duì)列頂部(mark_stack_tos),

否則使用使用函數(shù)allocate_in_condemned_generations計(jì)算把這個(gè)plug移動到前面(壓縮)時(shí)的偏移值,

allocate_in_condemned_generations的原理請看下圖


函數(shù)allocate_in_condemned_generations不會實(shí)際的移動內(nèi)存和修改指針,它只設(shè)置了plug的reloc成員,

這里需要注意的是如果有pinned plug并且前面的空間不夠,會從pinned plug的結(jié)尾開始計(jì)算,

同時(shí)出隊(duì)列以后的plug B在mark_stack_array中的len會被設(shè)置為前面一段空間的大小,也就是32+39=71。

現(xiàn)在讓我們思考一個(gè)問題,如果我們遇到一個(gè)對象x,如何求出對象x應(yīng)該移動到的位置?


我們需要根據(jù)對象x找到它所在的plug,然后根據(jù)這個(gè)plug的reloc移動,查找plug使用的索引就是接下來要說的Brick Table。

Brick Table

brick_table是一個(gè)類型為short*的數(shù)組,用于快速索引plug,如圖


根據(jù)所屬的brick不同,會構(gòu)建多個(gè)plug樹(避免plug樹過大),然后設(shè)置根節(jié)點(diǎn)的信息到brick_table中,

brick中的值如果是正值則表示brick對應(yīng)的開始地址離根節(jié)點(diǎn)plug的偏移值+1,

如果是負(fù)值則表示plug樹橫跨了多個(gè)brick,需要到前面的brick查找。

brick_table相關(guān)的代碼如下,我們可以看到在64位下brick的大小是4096,在32位下brick的大小是2048


brick_table中出現(xiàn)負(fù)值的情況是因?yàn)閜lug橫跨幅度比較大,超過了單個(gè)brick的時(shí)候后面的brick就會設(shè)為負(fù)值,

如果對象地址在上圖的1001或1002,查找這個(gè)對象對應(yīng)的plug會從1000的plug樹開始。

另外1002中的值不一定需要是-2,-1也是有效的,如果是-1會一直向前查找直到找到正值的brick。

在上面我們提到的問題可以通過brick_table解決,可以看下面relocate_address函數(shù)的代碼。

brick_table在gc過程中會儲存plug樹,但是在gc完成后(gc不執(zhí)行時(shí))會儲存各個(gè)brick中地址最大的plug,用于給find_first_object等函數(shù)定位對象的開始地址使用。

對于Pinned Plug的特殊處理

pinned plug除了會在plug樹和brick table中,還會保存在mark_stack_array隊(duì)列中,類型是mark。

因?yàn)閡npinned plug和pinned plug相鄰會導(dǎo)致原來的內(nèi)容被plug信息覆蓋,mark中還會保存以下的特殊信息

saved_pre_plug?

如果這個(gè)pinned plug覆蓋了上一個(gè)unpinned plug的結(jié)尾,這里會保存覆蓋前的原始內(nèi)容

saved_pre_plug_reloc?

同上,但是這個(gè)值用于重定位和壓縮階段(中間會交換)

saved_post_plug?

如果這個(gè)pinned plug被下一個(gè)unpinned plug覆蓋了結(jié)尾,這里會保存覆蓋前的原始內(nèi)容

saved_post_plug_reloc?

同上,但是這個(gè)值用于重定位和壓縮階段(中間會交換)

saved_pre_plug_info_reloc_start?

被覆蓋的saved_pre_plug內(nèi)容在重定位后的地址,如果重定位未發(fā)生則可以直接用(first - sizeof (plug_and_gap))

saved_post_plug_info_start?

被覆蓋的saved_post_plug內(nèi)容的地址,注意pinned plug不會被重定位

saved_pre_p?

是否保存了saved_pre_plug

如果覆蓋的內(nèi)容包含了對象的開頭(對象比較小,整個(gè)都被覆蓋了)

這里還會保存對象離各個(gè)引用成員的偏移值的bitmap (enque_pinned_plug)

saved_post_p?

是否保存了saved_post_p

如果覆蓋的內(nèi)容包含了對象的開頭(對象比較小,整個(gè)都被覆蓋了)

這里還會保存對象離各個(gè)引用成員的偏移值的bitmap (save_post_plug_info)

mark_stack_array中的len意義會在入隊(duì)和出隊(duì)時(shí)有所改變,

入隊(duì)時(shí)len代表pinned plug的大小,

出隊(duì)后len代表pinned plug離最后的模擬壓縮分配地址的空間(這個(gè)空間可以變成free object)。

mark_stack_array

mark_stack_array的結(jié)構(gòu)如下圖:


入隊(duì)時(shí)mark_stack_tos增加,出隊(duì)時(shí)mark_stack_bos增加,空間不夠時(shí)會擴(kuò)展然后mark_stack_array_length會增加。

計(jì)劃階段判斷使用壓縮(compact)還是清掃(sweep)的依據(jù)是什么

計(jì)劃階段模擬壓縮的時(shí)候創(chuàng)建plug,設(shè)置reloc等等只是為了接下來的壓縮做準(zhǔn)備,既不會修改指針地址也不會移動內(nèi)存。

在做完這些工作之后計(jì)劃階段會首先判斷應(yīng)不應(yīng)該進(jìn)行壓縮,如果不進(jìn)行壓縮而是進(jìn)行清掃,這些計(jì)算結(jié)果都會浪費(fèi)掉。

判斷是否使用壓縮的根據(jù)主要有

系統(tǒng)空余空閑是否過少,如果過少觸發(fā)swap可能會明顯的拖低性能,這時(shí)候應(yīng)該嘗試壓縮

碎片空間大小(fragmentation) >= 閾值(dd_fragmentation_limit)

碎片空間大小(fragmentation) / 收集代的大小(包括更年輕的代) >= 閾值(dd_fragmentation_burden_limit)

其他還有一些零碎的判斷,將在下面的decide_on_compacting函數(shù)的代碼中講解。

對象的升代與降代

在很多介紹.Net GC的書籍中都有提到過,經(jīng)過GC以后對象會升代,例如gen 0中的對象在一次GC后如果存活下來會變?yōu)間en 1。

在CoreCLR中,對象的升代需要滿足一定條件,某些特殊情況下不會升代,甚至?xí)荡?gen1變?yōu)間en0)。

對象升代的條件如下:

計(jì)劃階段(plan_phase)選擇清掃(sweep)時(shí)會啟用升代

入口點(diǎn)(garbage_collect)判斷當(dāng)前是Full GC時(shí)會啟用升代

dt_low_card_table_efficiency_p成立時(shí)會啟用升代?

請?jiān)谇懊娌檎襠t_low_card_table_efficiency_p查看該處的解釋

計(jì)劃階段(plan_phase)判斷上一代過小,或者這次標(biāo)記(存活)的對象過多時(shí)啟用升代?

請?jiān)诤竺娌檎襭romoted_bytes (i) > m查看該處的解釋

如果升代的條件不滿足,則原來在gen 0的對象GC后仍然會在gen 0,

某些特殊條件下還會發(fā)生降代,如下圖:


在模擬壓縮時(shí),原來在gen 1的對象會歸到gen 2(pinned object不一定),原來在gen 0的對象會歸到gen 1,

但是如果所有unpinned plug都已經(jīng)壓縮到前面,后面還有殘留的pinned plug時(shí),后面殘留的pinned plug中的對象則會不升代或者降代,

當(dāng)這種情況發(fā)生時(shí)計(jì)劃階段會設(shè)置demotion_low來標(biāo)記被降代的范圍。

如果最終選擇了清掃(sweep)則上圖中的情況不會發(fā)生。

計(jì)劃代邊界

計(jì)劃階段在模擬壓縮的時(shí)候還會計(jì)劃代邊界(generation::plan_allocation_start),

計(jì)劃代邊界的工作主要在process_ephemeral_boundaries, plan_generation_start, plan_generation_starts函數(shù)中完成。

大部分情況下函數(shù)process_ephemeral_boundaries會用來計(jì)劃gen 1的邊界,如果不升代這個(gè)函數(shù)還會計(jì)劃gen 0的邊界,

當(dāng)判斷當(dāng)前計(jì)劃的plug大于或等于下一代的邊界時(shí),例如大于等于gen 0的邊界時(shí)則會設(shè)置gen 1的邊界在這個(gè)plug的前面。

最終選擇壓縮(compact)時(shí),會把新的代邊界設(shè)置成計(jì)劃代邊界(請看fix_generation_bounds函數(shù)),

最終選擇清掃(sweep)時(shí),計(jì)劃代邊界不會被使用(請看make_free_lists函數(shù)和make_free_list_in_brick函數(shù))。

計(jì)劃階段(plan_phase)的代碼

gc_heap::plan_phase函數(shù)的代碼如下


計(jì)劃階段在模擬壓縮和判斷后會在內(nèi)部包含重定位階段(relocate_phase),壓縮階段(compact_phase)和清掃階段(sweep_phase)的處理,

接下來我們仔細(xì)分析一下這三個(gè)階段做了什么事情:

重定位階段(relocate_phase)

重定位階段的主要工作是修改對象的指針地址,例如A.Member的Member內(nèi)存移動后,A中指向Member的指針地址也需要改變。

重定位階段只會修改指針地址,復(fù)制內(nèi)存會交給下面的壓縮階段(compact_phase)完成。

如下圖:


圖中對象A和對象B引用了對象C,重定位后各個(gè)對象還在原來的位置,只是成員的地址(指針)變化了。

還記得之前標(biāo)記階段(mark_phase)使用的GcScanRoots等掃描函數(shù)嗎?

這些掃描函數(shù)同樣會在重定位階段使用,只是執(zhí)行的不是GCHeap::Promote而是GCHeap::Relocate。

重定位對象會借助計(jì)劃階段(plan_phase)構(gòu)建的brick table和plug樹來進(jìn)行快速的定位,然后對指針地址移動所屬plug的reloc位置。

重定位階段(relocate_phase)的代碼


重定位階段(relocate_phase)只是修改了引用對象的地址,對象還在原來的位置,接下來進(jìn)入壓縮階段(compact_phase):

壓縮階段(compact_phase)

壓縮階段負(fù)責(zé)把對象復(fù)制到之前模擬壓縮到的地址上,簡單點(diǎn)來講就是用memcpy復(fù)制這些對象到新的地址。

壓縮階段會使用之前構(gòu)建的brick table和plug樹快速的枚舉對象。

gc_heap::compact_phase函數(shù)的代碼如下:

這個(gè)函數(shù)的代碼是不是有點(diǎn)眼熟?它的流程和上面的relocate_survivors很像,都是枚舉brick table然后中序枚舉plug樹


壓縮階段結(jié)束以后還需要做一些收尾工作,請從上面plan_phase中的recover_saved_pinned_info();繼續(xù)看。

參考鏈接

https://github.com/dotnet/coreclr/blob/master/Documentation/botr/garbage-collection.md

https://raw.githubusercontent.com/dotnet/coreclr/release/1.1.0/src/gc/gc.cpp

https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcimpl.h

https://github.com/dotnet/coreclr/blob/release/1.1.0/src/gc/gcpriv.h

https://github.com/dotnet/coreclr/issues/8959

https://github.com/dotnet/coreclr/issues/8995

https://github.com/dotnet/coreclr/issues/9053

https://github.com/dotnet/coreclr/issues/10137

https://github.com/dotnet/coreclr/issues/10305

https://github.com/dotnet/coreclr/issues/10141

寫在最后

GC的實(shí)際處理遠(yuǎn)遠(yuǎn)比文檔和書中寫的要復(fù)雜,希望這一篇文章可以讓你更加深入的理解CoreCLR,如果你發(fā)現(xiàn)了錯(cuò)誤或者有疑問的地方請指出來,

另外這篇文章有一些部分尚未涵蓋到,例如SuspendEE的原理,后臺GC的處理和stackwalking等,希望以后可以再花時(shí)間去研究它們。

下一篇我將會實(shí)際使用LLDB跟蹤GC收集垃圾的處理,再下一篇會寫JIT相關(guān)的內(nèi)容,敬請期待

相關(guān)的代碼太多,請閱讀原文。

相關(guān)文章:

  • 《代碼的未來》讀書筆記:內(nèi)存管理與GC那點(diǎn)事兒

  • CoreCLR源碼探索(一) Object是什么

  • CoreCLR源碼探索(二) new是什么

  • CoreCLR源碼探索(三) GC內(nèi)存分配器的內(nèi)部實(shí)現(xiàn)

  • .NET跨平臺之旅:corehost 是如何加載 coreclr 的

  • .NET CoreCLR開發(fā)人員指南(上)

原文地址:http://www.cnblogs.com/zkweb/p/6625049.html


.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關(guān)注

總結(jié)

以上是生活随笔為你收集整理的CoreCLR源码探索(四) GC内存收集器的内部实现 分析篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。