JVM源碼分析之YGC的來龍去脈

簡書 占小狼
轉載請注明原創出處,謝謝

換了新工作,確實比以前忙多了,從而也擱置了自己興趣,不過還是想方設法的擠出一點時間把YGC的一些細節實現重新看了幾遍,HotSpot里的不少代碼寫的太糾結,山路十八彎,要理清楚確實需要費點時間。

YGC是JVM GC當前最為頻繁的一種GC,一個高并發的服務在運行期間,會進行大量的YGC,發生YGC時,會進行STW,一般時間都很短,除非碰到YGC時,存在大量的存活對象需要進行拷貝。

一次YGC過程主要分成兩個步驟:
1、查找GC Roots,拷貝所引用的對象到 to 區;
2、遞歸遍歷步驟1中對象,并拷貝其所引用的對象到 to 區,當然可能會存在自然晉升,或者因為 to 區空間不足引起的提前晉升的情況;

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

查找GC Roots

YGC的第一步根據GC Roots找出第一批活躍的對象,Hotspot中通過gch->gen_process_strong_roots方法實現

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

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

YGC在執行時只收集young generation,不收集old generation和perm generation,并不會做類的卸載行為,所以上述可選部分都作為Strong root,但是在FGC時就不會當作Strong root了。

紅色框中的實現邏輯對于YGC來說是沒有意義的,因為level=0,Hotspot中唯一用到這個地方的只有CMS GC實現,默認只收集old generation,所以需要掃描young generation作為它的Strong root。

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

遍歷活躍對象

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

遍歷過程的實現由FastEvacuateFollowersClosure類的do_void方法完成,這又是一個*-Closure 方式命名的類,實現如下

每個內存區域都有兩個指針變量,分別是 _saved_mark_word 和 _top,其中_saved_mark_word 指向當前遍歷對象的位置,_top指向當前內存區域可分配的位置,其中_saved_mark_word 到 _top之間的對象是已拷貝,但未掃描的對象。


GC Roots引用的對象拷貝完成后,to-space的_saved_mark_word和_top的狀態如上圖所示,假設期間沒有對象晉升到old generation。每次掃描一個對象,_saved_mark_word會往前移動,期間也有新的對象會被拷貝到to-space,_top也會往前移動,直到_saved_mark_word追上_top,說明to-space的對象都已經遍歷完成。

其中while循環條件 while (!_gch->no_allocs_since_save_marks(_level),就是在判斷各個內存代中的_saved_mark_word是否已經追到_top,如果還沒有追上,就執行_gch->oop_since_save_marks_iterate進行遍歷,實現如下:

從代碼實現可以看出對新生代、老年代和永久代都會進行遍歷,其中新生代的遍歷實現如下:

這里會對eden、from和to分別進行遍歷,第一次看這塊邏輯的時候很納悶,為什么要對eden和from-space進行遍歷,from倒沒什么問題,_saved_mark_word和_top一般都是相同的,但是eden區的_saved_mark_word明顯不會等于_top,一直沒有找到在eden區分配對象時,改變_top的同時也改變_saved_mark_word的邏輯,后來發現GenCollectedHeap::do_collection方法中,在調用各個代的collect之前,會調用save_marks()方法,將_saved_mark_word設置為_top,這樣在發生YGC時,eden區的對象其實是不會被遍歷的,被這個疑惑困擾了好久,結果是個遺留代碼。

to-space對象的遍歷實現:


這里的blk變量是傳遞過來的FastScanClosure回調函數,oop_iterate方法會遍歷該對象的所有引用,并調用回調函數的do_oop_work方法處理這里引用所指向的對象。

do_oop_work的實現

在FastScanClosure回調函數的do_oop_work方法實現中,紅框的是重要的部分,因為可能存在多個對象共同引用一個對象,所以在遍歷過程中,可能會遇到已經處理過的對象,如果遇到這樣的對象,就不會再次進行復制了,如果該對象沒有被拷貝過,則調用 copy_to_survivor_space 方法拷貝對象到to-space或者晉升到old generation,這里提一下ParNew的實現,因為是并發執行的,所以可能存在多個線程拷貝了同一個對象到to-space,不過通過原子操作,保證了只有一個對象是有效的。

copy_to_survivor_space 的實現:

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


個人公眾號


如果該對象的年齡大于某個閾值,會晉升到old generation,或者在拷貝到to-space時空間不足,也會提前晉升到old generation,晉升過程通過老年代_next_gen的promote方法實現,如果old generation也沒有足夠的空間容納該對象,則會觸發晉升失敗。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容