簡書 占小狼
轉載請注明原創出處,謝謝
換了新工作,確實比以前忙多了,從而也擱置了自己興趣,不過還是想方設法的擠出一點時間把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也沒有足夠的空間容納該對象,則會觸發晉升失敗。