JVM源碼分析之YGC的來(lái)龍去脈

簡(jiǎn)書(shū) 占小狼
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處,謝謝

換了新工作,確實(shí)比以前忙多了,從而也擱置了自己興趣,不過(guò)還是想方設(shè)法的擠出一點(diǎn)時(shí)間把YGC的一些細(xì)節(jié)實(shí)現(xiàn)重新看了幾遍,HotSpot里的不少代碼寫(xiě)的太糾結(jié),山路十八彎,要理清楚確實(shí)需要費(fèi)點(diǎn)時(shí)間。

YGC是JVM GC當(dāng)前最為頻繁的一種GC,一個(gè)高并發(fā)的服務(wù)在運(yùn)行期間,會(huì)進(jìn)行大量的YGC,發(fā)生YGC時(shí),會(huì)進(jìn)行STW,一般時(shí)間都很短,除非碰到Y(jié)GC時(shí),存在大量的存活對(duì)象需要進(jìn)行拷貝。

一次YGC過(guò)程主要分成兩個(gè)步驟:
1、查找GC Roots,拷貝所引用的對(duì)象到 to 區(qū);
2、遞歸遍歷步驟1中對(duì)象,并拷貝其所引用的對(duì)象到 to 區(qū),當(dāng)然可能會(huì)存在自然晉升,或者因?yàn)?to 區(qū)空間不足引起的提前晉升的情況;

下面進(jìn)行分析的是Serial GC,ParNew GC可以理解成并發(fā)的Serial GC,實(shí)現(xiàn)原理都差不多,看源碼的話(huà)建議看Serial GC 的實(shí)現(xiàn)類(lèi)DefNewGeneration,畢竟單線(xiàn)程實(shí)現(xiàn)的復(fù)雜性會(huì)低一點(diǎn),在DefNewGeneration中,會(huì)看到一些以 *-Closure 方式命名的類(lèi),這些都是封裝起來(lái)的回調(diào)函數(shù),是為了讓GC的具體邏輯與對(duì)象內(nèi)部的字段遍歷邏輯能夠松耦合,比如ScanClosure 與 FastScanClosure 作為回調(diào)函數(shù)傳入到各個(gè)方法中,實(shí)現(xiàn)GC實(shí)現(xiàn)的對(duì)象遍歷,正因?yàn)檫@種實(shí)現(xiàn)方式,大大增加了閱讀源碼的難度。

查找GC Roots

YGC的第一步根據(jù)GC Roots找出第一批活躍的對(duì)象,Hotspot中通過(guò)gch->gen_process_strong_roots方法實(shí)現(xiàn)

在黃色框的實(shí)現(xiàn)中,SharedHeap::process_strong_roots()掃描了所有一定是GC Roots的內(nèi)存區(qū)域,有興趣的可以查看process_strong_roots的實(shí)現(xiàn),主要包括了以下東西:

  • Universe類(lèi)中所引用的一些必須存活的對(duì)象 Universe::oops_do(roots)
  • 所有JNI Handles JNIHandles::oops_do(roots)
  • 所有線(xiàn)程的棧 Threads::oops_do(roots, code_roots)
  • 所有被Synchronize鎖持有的對(duì)象 ObjectSynchronizer::oops_do(roots)
  • VM內(nèi)實(shí)現(xiàn)的MBean所持有的對(duì)象 Management::oops_do(roots)
  • JVMTI所持有的對(duì)象 JvmtiExport::oops_do(roots)
  • (可選)所有已加載的類(lèi) 或 所有已加載的系統(tǒng)類(lèi) SystemDictionary::oops_do(roots)
  • (可選)所有駐留字符串(StringTable) StringTable::oops_do(roots)
  • (可選)代碼緩存(CodeCache) CodeCache::scavenge_root_nmethods_do(code_roots)
  • (可選)PermGen的remember set所記錄的存在跨代引用的區(qū)域 rem_set()->younger_refs_iterate(perm_gen(), perm_blk)

YGC在執(zhí)行時(shí)只收集young generation,不收集old generation和perm generation,并不會(huì)做類(lèi)的卸載行為,所以上述可選部分都作為Strong root,但是在FGC時(shí)就不會(huì)當(dāng)作Strong root了。

紅色框中的實(shí)現(xiàn)邏輯對(duì)于YGC來(lái)說(shuō)是沒(méi)有意義的,因?yàn)閘evel=0,Hotspot中唯一用到這個(gè)地方的只有CMS GC實(shí)現(xiàn),默認(rèn)只收集old generation,所以需要掃描young generation作為它的Strong root。

講到這里,似乎有一部分被忽略了,如果一個(gè)old generation的對(duì)象引用了young generation,那么這個(gè)old generation的對(duì)象肯定也屬于Strong root的一部分,這部分邏輯并沒(méi)有在process_strong_roots中實(shí)現(xiàn),而是在綠色框中實(shí)現(xiàn)了,其中rem_set中保存了old generation中dirty card的對(duì)應(yīng)區(qū)域,每次對(duì)象的拷貝移動(dòng)都會(huì)檢查一下是否產(chǎn)生了新的跨代引用,比如有對(duì)象晉升到了old generation,而該對(duì)象還引用了young generation的對(duì)象,這種情況下會(huì)把相應(yīng)的card置為dirty,下次YGC的時(shí)候只會(huì)掃描dirty card所指內(nèi)存的對(duì)象,避免掃描所有的old generation對(duì)象。

遍歷活躍對(duì)象

在查找GC Roots的步驟中,已經(jīng)找出了第一批存活的對(duì)象,這些存活對(duì)象可能在 to-space,也有可能直接晉升到了 old generation,這些區(qū)域都是需要進(jìn)行遍歷的,保證所有的活躍對(duì)象都能存活下來(lái)。

遍歷過(guò)程的實(shí)現(xiàn)由FastEvacuateFollowersClosure類(lèi)的do_void方法完成,這又是一個(gè)*-Closure 方式命名的類(lèi),實(shí)現(xiàn)如下

每個(gè)內(nèi)存區(qū)域都有兩個(gè)指針變量,分別是 _saved_mark_word 和 _top,其中_saved_mark_word 指向當(dāng)前遍歷對(duì)象的位置,_top指向當(dāng)前內(nèi)存區(qū)域可分配的位置,其中_saved_mark_word 到 _top之間的對(duì)象是已拷貝,但未掃描的對(duì)象。


GC Roots引用的對(duì)象拷貝完成后,to-space的_saved_mark_word和_top的狀態(tài)如上圖所示,假設(shè)期間沒(méi)有對(duì)象晉升到old generation。每次掃描一個(gè)對(duì)象,_saved_mark_word會(huì)往前移動(dòng),期間也有新的對(duì)象會(huì)被拷貝到to-space,_top也會(huì)往前移動(dòng),直到_saved_mark_word追上_top,說(shuō)明to-space的對(duì)象都已經(jīng)遍歷完成。

其中while循環(huán)條件 while (!_gch->no_allocs_since_save_marks(_level),就是在判斷各個(gè)內(nèi)存代中的_saved_mark_word是否已經(jīng)追到_top,如果還沒(méi)有追上,就執(zhí)行_gch->oop_since_save_marks_iterate進(jìn)行遍歷,實(shí)現(xiàn)如下:

從代碼實(shí)現(xiàn)可以看出對(duì)新生代、老年代和永久代都會(huì)進(jìn)行遍歷,其中新生代的遍歷實(shí)現(xiàn)如下:

這里會(huì)對(duì)eden、from和to分別進(jìn)行遍歷,第一次看這塊邏輯的時(shí)候很納悶,為什么要對(duì)eden和from-space進(jìn)行遍歷,from倒沒(méi)什么問(wèn)題,_saved_mark_word和_top一般都是相同的,但是eden區(qū)的_saved_mark_word明顯不會(huì)等于_top,一直沒(méi)有找到在eden區(qū)分配對(duì)象時(shí),改變_top的同時(shí)也改變_saved_mark_word的邏輯,后來(lái)發(fā)現(xiàn)GenCollectedHeap::do_collection方法中,在調(diào)用各個(gè)代的collect之前,會(huì)調(diào)用save_marks()方法,將_saved_mark_word設(shè)置為_(kāi)top,這樣在發(fā)生YGC時(shí),eden區(qū)的對(duì)象其實(shí)是不會(huì)被遍歷的,被這個(gè)疑惑困擾了好久,結(jié)果是個(gè)遺留代碼。

to-space對(duì)象的遍歷實(shí)現(xiàn):


這里的blk變量是傳遞過(guò)來(lái)的FastScanClosure回調(diào)函數(shù),oop_iterate方法會(huì)遍歷該對(duì)象的所有引用,并調(diào)用回調(diào)函數(shù)的do_oop_work方法處理這里引用所指向的對(duì)象。

do_oop_work的實(shí)現(xiàn)

在FastScanClosure回調(diào)函數(shù)的do_oop_work方法實(shí)現(xiàn)中,紅框的是重要的部分,因?yàn)榭赡艽嬖诙鄠€(gè)對(duì)象共同引用一個(gè)對(duì)象,所以在遍歷過(guò)程中,可能會(huì)遇到已經(jīng)處理過(guò)的對(duì)象,如果遇到這樣的對(duì)象,就不會(huì)再次進(jìn)行復(fù)制了,如果該對(duì)象沒(méi)有被拷貝過(guò),則調(diào)用 copy_to_survivor_space 方法拷貝對(duì)象到to-space或者晉升到old generation,這里提一下ParNew的實(shí)現(xiàn),因?yàn)槭遣l(fā)執(zhí)行的,所以可能存在多個(gè)線(xiàn)程拷貝了同一個(gè)對(duì)象到to-space,不過(guò)通過(guò)原子操作,保證了只有一個(gè)對(duì)象是有效的。

copy_to_survivor_space 的實(shí)現(xiàn):

拷貝對(duì)象的目標(biāo)空間不一定是to-space,也有可能是old generation,如果一個(gè)對(duì)象經(jīng)歷了很多次YGC,會(huì)從young generation直接晉升到old generation,為了記錄對(duì)象經(jīng)歷的YGC次數(shù),在對(duì)象頭的mark word 數(shù)據(jù)結(jié)構(gòu)中有一個(gè)位置記錄著對(duì)象的YGC次數(shù),也叫對(duì)象的年齡,如果掃描到的對(duì)象,其年齡小于某個(gè)閾值(tenuring threshold),該對(duì)象會(huì)被拷貝到to-space,并增加該對(duì)象的年齡,同時(shí)to-space的_top指針也會(huì)往后移動(dòng),這個(gè)新對(duì)象等待著被掃描。


個(gè)人公眾號(hào)


如果該對(duì)象的年齡大于某個(gè)閾值,會(huì)晉升到old generation,或者在拷貝到to-space時(shí)空間不足,也會(huì)提前晉升到old generation,晉升過(guò)程通過(guò)老年代_next_gen的promote方法實(shí)現(xiàn),如果old generation也沒(méi)有足夠的空間容納該對(duì)象,則會(huì)觸發(fā)晉升失敗。

參考
http://hllvm.group.iteye.com/group/topic/39376

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容