簡書 占小狼
轉(zhuǎn)載請注明原創(chuàng)出處,謝謝
換了新工作阶捆,確實(shí)比以前忙多了凌节,從而也擱置了自己興趣,不過還是想方設(shè)法的擠出一點(diǎn)時(shí)間把YGC的一些細(xì)節(jié)實(shí)現(xiàn)重新看了幾遍洒试,HotSpot里的不少代碼寫的太糾結(jié)倍奢,山路十八彎,要理清楚確實(shí)需要費(fèi)點(diǎn)時(shí)間垒棋。
YGC是JVM GC當(dāng)前最為頻繁的一種GC卒煞,一個(gè)高并發(fā)的服務(wù)在運(yùn)行期間,會進(jìn)行大量的YGC叼架,發(fā)生YGC時(shí)畔裕,會進(jìn)行STW,一般時(shí)間都很短乖订,除非碰到Y(jié)GC時(shí)扮饶,存在大量的存活對象需要進(jìn)行拷貝。
一次YGC過程主要分成兩個(gè)步驟:
1乍构、查找GC Roots贴届,拷貝所引用的對象到 to 區(qū);
2蜡吧、遞歸遍歷步驟1中對象毫蚓,并拷貝其所引用的對象到 to 區(qū),當(dāng)然可能會存在自然晉升昔善,或者因?yàn)?to 區(qū)空間不足引起的提前晉升的情況元潘;
下面進(jìn)行分析的是Serial GC,ParNew GC可以理解成并發(fā)的Serial GC君仆,實(shí)現(xiàn)原理都差不多翩概,看源碼的話建議看Serial GC 的實(shí)現(xiàn)類DefNewGeneration牲距,畢竟單線程實(shí)現(xiàn)的復(fù)雜性會低一點(diǎn),在DefNewGeneration中钥庇,會看到一些以 *-Closure 方式命名的類牍鞠,這些都是封裝起來的回調(diào)函數(shù),是為了讓GC的具體邏輯與對象內(nèi)部的字段遍歷邏輯能夠松耦合评姨,比如ScanClosure 與 FastScanClosure 作為回調(diào)函數(shù)傳入到各個(gè)方法中难述,實(shí)現(xiàn)GC實(shí)現(xiàn)的對象遍歷,正因?yàn)檫@種實(shí)現(xiàn)方式吐句,大大增加了閱讀源碼的難度胁后。
查找GC Roots
YGC的第一步根據(jù)GC Roots找出第一批活躍的對象,Hotspot中通過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類中所引用的一些必須存活的對象
Universe::oops_do(roots)
- 所有JNI Handles
JNIHandles::oops_do(roots)
- 所有線程的棧
Threads::oops_do(roots, code_roots)
- 所有被Synchronize鎖持有的對象
ObjectSynchronizer::oops_do(roots)
- VM內(nèi)實(shí)現(xiàn)的MBean所持有的對象
Management::oops_do(roots)
- JVMTI所持有的對象
JvmtiExport::oops_do(roots)
- (可選)所有已加載的類 或 所有已加載的系統(tǒng)類
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侣诺,并不會做類的卸載行為,所以上述可選部分都作為Strong root氧秘,但是在FGC時(shí)就不會當(dāng)作Strong root了年鸳。
紅色框中的實(shí)現(xiàn)邏輯對于YGC來說是沒有意義的,因?yàn)閘evel=0敏储,Hotspot中唯一用到這個(gè)地方的只有CMS GC實(shí)現(xiàn),默認(rèn)只收集old generation朋鞍,所以需要掃描young generation作為它的Strong root已添。
講到這里,似乎有一部分被忽略了滥酥,如果一個(gè)old generation的對象引用了young generation更舞,那么這個(gè)old generation的對象肯定也屬于Strong root的一部分,這部分邏輯并沒有在process_strong_roots
中實(shí)現(xiàn)坎吻,而是在綠色框中實(shí)現(xiàn)了缆蝉,其中rem_set中保存了old generation中dirty card的對應(yīng)區(qū)域,每次對象的拷貝移動都會檢查一下是否產(chǎn)生了新的跨代引用瘦真,比如有對象晉升到了old generation刊头,而該對象還引用了young generation的對象,這種情況下會把相應(yīng)的card置為dirty诸尽,下次YGC的時(shí)候只會掃描dirty card所指內(nèi)存的對象原杂,避免掃描所有的old generation對象。
遍歷活躍對象
在查找GC Roots的步驟中您机,已經(jīng)找出了第一批存活的對象穿肄,這些存活對象可能在 to-space年局,也有可能直接晉升到了 old generation,這些區(qū)域都是需要進(jìn)行遍歷的咸产,保證所有的活躍對象都能存活下來矢否。
遍歷過程的實(shí)現(xiàn)由FastEvacuateFollowersClosure類的do_void方法完成,這又是一個(gè)*-Closure 方式命名的類脑溢,實(shí)現(xiàn)如下
每個(gè)內(nèi)存區(qū)域都有兩個(gè)指針變量僵朗,分別是 _saved_mark_word 和 _top,其中_saved_mark_word 指向當(dāng)前遍歷對象的位置焚志,_top指向當(dāng)前內(nèi)存區(qū)域可分配的位置衣迷,其中_saved_mark_word 到 _top之間的對象是已拷貝,但未掃描的對象酱酬。
GC Roots引用的對象拷貝完成后壶谒,to-space的_saved_mark_word和_top的狀態(tài)如上圖所示,假設(shè)期間沒有對象晉升到old generation膳沽。每次掃描一個(gè)對象汗菜,_saved_mark_word會往前移動,期間也有新的對象會被拷貝到to-space挑社,_top也會往前移動陨界,直到_saved_mark_word追上_top,說明to-space的對象都已經(jīng)遍歷完成痛阻。
其中while循環(huán)條件 while (!_gch->no_allocs_since_save_marks(_level)
菌瘪,就是在判斷各個(gè)內(nèi)存代中的_saved_mark_word是否已經(jīng)追到_top,如果還沒有追上阱当,就執(zhí)行_gch->oop_since_save_marks_iterate
進(jìn)行遍歷俏扩,實(shí)現(xiàn)如下:
從代碼實(shí)現(xiàn)可以看出對新生代、老年代和永久代都會進(jìn)行遍歷弊添,其中新生代的遍歷實(shí)現(xiàn)如下:
這里會對eden录淡、from和to分別進(jìn)行遍歷,第一次看這塊邏輯的時(shí)候很納悶油坝,為什么要對eden和from-space進(jìn)行遍歷嫉戚,from倒沒什么問題,_saved_mark_word和_top一般都是相同的澈圈,但是eden區(qū)的_saved_mark_word明顯不會等于_top彬檀,一直沒有找到在eden區(qū)分配對象時(shí),改變_top的同時(shí)也改變_saved_mark_word的邏輯瞬女,后來發(fā)現(xiàn)GenCollectedHeap::do_collection
方法中凤覆,在調(diào)用各個(gè)代的collect之前,會調(diào)用save_marks()方法拆魏,將_saved_mark_word設(shè)置為_top盯桦,這樣在發(fā)生YGC時(shí)慈俯,eden區(qū)的對象其實(shí)是不會被遍歷的,被這個(gè)疑惑困擾了好久拥峦,結(jié)果是個(gè)遺留代碼贴膘。
to-space對象的遍歷實(shí)現(xiàn):
這里的blk變量是傳遞過來的FastScanClosure回調(diào)函數(shù),oop_iterate方法會遍歷該對象的所有引用略号,并調(diào)用回調(diào)函數(shù)的do_oop_work方法處理這里引用所指向的對象刑峡。
do_oop_work
的實(shí)現(xiàn)
在FastScanClosure回調(diào)函數(shù)的do_oop_work方法實(shí)現(xiàn)中,紅框的是重要的部分玄柠,因?yàn)榭赡艽嬖诙鄠€(gè)對象共同引用一個(gè)對象突梦,所以在遍歷過程中,可能會遇到已經(jīng)處理過的對象羽利,如果遇到這樣的對象宫患,就不會再次進(jìn)行復(fù)制了,如果該對象沒有被拷貝過这弧,則調(diào)用 copy_to_survivor_space
方法拷貝對象到to-space或者晉升到old generation娃闲,這里提一下ParNew的實(shí)現(xiàn),因?yàn)槭遣l(fā)執(zhí)行的匾浪,所以可能存在多個(gè)線程拷貝了同一個(gè)對象到to-space皇帮,不過通過原子操作,保證了只有一個(gè)對象是有效的蛋辈。
copy_to_survivor_space
的實(shí)現(xiàn):
拷貝對象的目標(biāo)空間不一定是to-space属拾,也有可能是old generation,如果一個(gè)對象經(jīng)歷了很多次YGC冷溶,會從young generation直接晉升到old generation渐白,為了記錄對象經(jīng)歷的YGC次數(shù),在對象頭的mark word 數(shù)據(jù)結(jié)構(gòu)中有一個(gè)位置記錄著對象的YGC次數(shù)挂洛,也叫對象的年齡礼预,如果掃描到的對象眠砾,其年齡小于某個(gè)閾值(tenuring threshold)虏劲,該對象會被拷貝到to-space,并增加該對象的年齡褒颈,同時(shí)to-space的_top指針也會往后移動柒巫,這個(gè)新對象等待著被掃描。
個(gè)人公眾號
如果該對象的年齡大于某個(gè)閾值谷丸,會晉升到old generation堡掏,或者在拷貝到to-space時(shí)空間不足,也會提前晉升到old generation刨疼,晉升過程通過老年代_next_gen的promote方法實(shí)現(xiàn)泉唁,如果old generation也沒有足夠的空間容納該對象鹅龄,則會觸發(fā)晉升失敗。