曾經(jīng)栋荸,我以為這些東西自己平時看看書就夠了菇怀,屬于那種花了半天精力總算搞明白了,然后過兩天就自然忘記的東西晌块。
結(jié)果爱沟,這都啥啊,啥是卡表匆背,什么又是三色標(biāo)記法呼伸,這些鬼問題都有人面試問,卷就完了。
引用計數(shù)&可達性分析
要進行垃圾回收GC括享,那么我們首先就要決定到底怎么判斷對象是否存活搂根?一般來說有兩種方式。
引用計數(shù)铃辖,給對象添加一個計數(shù)器剩愧,每當(dāng)有地方引用它計數(shù)器就+1,反之引用失效時就-1联逻,那么計數(shù)器值為0的對象就是可以回收的對象从祝,但是有一個問題就是循環(huán)引用的話無法解決弃酌。
對于現(xiàn)在的虛擬機來說,主要用的算法是可達性分析算法锦积。
首先定義GC ROOTS根對象集合,通過GC ROOTS向下搜索瓶殃,搜索的過程走過的路徑稱作引用鏈充包,如果某個對象到GC ROOTS沒有任何引用鏈,那么就是對象不可達遥椿,是可以被回收的對象基矮。
不可達對象需要進行兩次標(biāo)記,第一次發(fā)現(xiàn)沒有引用鏈相連冠场,會被第一次標(biāo)記家浇,如果需要執(zhí)行finalize()方法,之后這個對象會被放進隊列中等待執(zhí)行finalize()碴裙,如果在finalize()中成功和引用鏈上的其他對象關(guān)聯(lián)钢悲,就會被移出可回收對象集合。(但是不建議使用finalize()方法)
分代收集
有了如何判斷對象存活的基礎(chǔ)舔株,接下來的問題就是怎么進行垃圾收集GC莺琳,現(xiàn)在商用的虛擬機基本上都是分代收集的實現(xiàn),它的實現(xiàn)建立于兩個假說:
- 絕大多數(shù)對象都是朝生夕死的
- 熬過越多次垃圾回收的對象越難死亡
基于這兩個假說载慈,就產(chǎn)生了現(xiàn)在我們常見的年輕代和老年代惭等。
因為分代了,所以GC也就分代了办铡。
年輕代用于存放那些死的快的對象辞做,年輕代GC我們稱之為MinorGC,每次年輕代內(nèi)存不夠我們就觸發(fā)MinorGC寡具,以后還有存活的對象我們就根據(jù)經(jīng)歷過MinorGC次數(shù)和動態(tài)年齡判斷來決定是否晉升老年代秤茅。
老年代則存放老不死的對象,這里GC稱之為OldGC童叠,現(xiàn)在也有很多人把他叫做FullGC框喳,實際上這并不準(zhǔn)確,F(xiàn)ullGC應(yīng)該泛指年輕代和老年代的的GC。
按照我們上文所說的使用可達性分析算法來判斷對象的存活帖努,那么假如我們進行MinorGC撰豺,會不會有對象被老年代引用著?進行OldGC會不會又有對象被年輕代引用著拼余?
如果是的話污桦,那我們進行MinorGC的時候不光要管GC Roots,還有再去遍歷老年代匙监,這個性能問題就很大了凡橱。
因此,又來了一個假說亭姥。稼钩。。
跨代引用相對于同代引用來說僅占極少數(shù)达罗。
由此就產(chǎn)生了一個新的解決方案坝撑,我們不用去掃描整個老年代了,只要在年輕代建立一個數(shù)據(jù)結(jié)構(gòu)粮揉,叫做記憶集Remembered Set巡李,他把老年代劃分為N個區(qū)域,標(biāo)志出哪個區(qū)域會存在跨代引用扶认。
以后在進行MinorGC的時候侨拦,只要把這些包含了跨代引用的內(nèi)存區(qū)域加入GC Roots一起掃描就行了。
卡表
說完這些辐宾,才到了第一個話題:卡表狱从。
卡表實際上就是記憶集的一種實現(xiàn)方式,如果說記憶集是接口的話叠纹,那么卡表就是他的實現(xiàn)類季研。
對于HotSpot虛擬機來說,卡表的實現(xiàn)方式就是一個字節(jié)數(shù)組誉察。
CARD_TABLE [this address >> 9] = 0;
這段代碼代表著卡表標(biāo)記的的邏輯与涡。實際上卡表就是映射了一塊塊的內(nèi)存地址,這些內(nèi)存地址塊稱為卡頁冒窍,從代碼可以看出每個卡頁的大小就是2^9=512字節(jié)。
如果轉(zhuǎn)換為16進制豺鼻,數(shù)組的0综液,1號元素就映射為0x0000~0x01FF(0-511)、0x0200~0x03FF(512-1023)內(nèi)存地址的卡頁儒飒。
只要一個卡頁內(nèi)的對象存在一個或者多個跨代對象指針谬莹,就將該位置的卡表數(shù)組元素修改為1,表示這個位置為臟,沒有則為0附帽。
在GC的時候埠戳,就直接把值為1對應(yīng)的卡頁對象指針加入GC Roots一起掃描即可。
有了卡表蕉扮,我們就不需要去在發(fā)生MinorGC的時候掃描整個老年代了整胃,性能得到了極大的提升。
卡表的問題
寫屏障
卡表的數(shù)組元素要修改成1喳钟,也就是臟的狀態(tài)屁使,對于HotSpot來說是通過寫屏障來實現(xiàn)的,實際上就是在其他分代引用了當(dāng)前分代的對象時候奔则,在對引用進行賦值的時候進行更新蛮寂,更新的方式類似AOP的切面思想。
void oop_field_store(oop* field, oop new_value) {
// 引用字段賦值操作
*field = new_value;
// 寫后屏障易茬,在這里完成卡表狀態(tài)更新
post_write_barrier(field, new_value);
}
寫屏障帶來的問題就是額外的性能開銷酬蹋,不過這個問題不大,還能接受抽莱。
偽共享
緩存行通常來說都是64字節(jié)范抓,一個卡表元素1個字節(jié),占用的卡頁內(nèi)存大小就是64*512=32KB的大小岸蜗。
如果多線程剛好更新剛好處于這32KB范圍內(nèi)的對象尉咕,那么就會對性能產(chǎn)生影響。
怎么解決偽共享問題璃岳?
JDK7之后新增了一個參數(shù)-XX:+UseCondCardMark年缎,他代表是否開啟卡表更新的判斷,沒有被標(biāo)記過才標(biāo)記為臟铃慷。
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
三色標(biāo)記法
卡表解決了跨代收集和根節(jié)點枚舉的性能問題单芜。而有了這些措施實際上枚舉根節(jié)點這個過程造成的STW停頓已經(jīng)屬于可控范圍。
另外還存在一個問題就是接下來從GC Roots開始遍歷犁柜,怎么才能高效的標(biāo)記這些對象洲鸠,這就是三色標(biāo)記法的作用了。因為如果堆內(nèi)的對象越多馋缅,那么顯然標(biāo)記產(chǎn)生的停頓時間就越長扒腕。
以現(xiàn)在我們熟知的CMS或者G1來舉例,GC的前兩個步驟如下:
- 初始標(biāo)記:標(biāo)記GC ROOT能關(guān)聯(lián)到的對象萤悴,這一步需要STW瘾腰,但是停頓的時間很短。
- 并發(fā)標(biāo)記:從GCRoots的直接關(guān)聯(lián)對象開始遍歷整個對象圖的過程覆履,這個時間會比較長蹋盆,但是現(xiàn)在是可以和用戶線程并發(fā)執(zhí)行的费薄,這個效率的問題就是三色標(biāo)記關(guān)注的問題。
在三色標(biāo)記法中栖雾,把從GC Roots開始遍歷的對象標(biāo)記為以下三種顏色:
- 白色楞抡,在剛開始遍歷的時候,所有的對象都是白色的
- 灰色析藕,被垃圾回收器掃描過召廷,但是至少還有一個引用沒有被掃描
- 黑色,被垃圾回收器掃描過噪径,并且這個對象的引用也全部都被掃描過柱恤,是安全存活的對象
整個標(biāo)記的過程如下,首先剛開始從GC Roots開始遍歷的時候肯定所有的對象都是白色的找爱。
接著A\G對象被掃描到變成灰色梗顺,然后A\G對象的引用也都被掃描,A\G對象變成黑色车摄。
B\C對象開始被掃描變成灰色寺谤,他們的引用也被掃描完成后自己也就都變成了黑色。
而后D對象也一樣會經(jīng)歷從灰色到黑色的過程(偷點懶吮播,省略一張無關(guān)緊要的過程圖吧)
三色標(biāo)記的問題
雖然三色標(biāo)記法很高效,但是也會引申出其他的問題意狠。
首先我們上文說過并發(fā)標(biāo)記的過程是不會STW的粟关,就是你媽在打掃衛(wèi)生,而你在旁邊一直丟垃圾环戈,這也沒關(guān)系闷板,大不了最后就是還有一些垃圾沒掃干凈而已。
對于三色標(biāo)記來說就是把應(yīng)該要清理的對象標(biāo)記成存活院塞,這樣本次GC就無法清理這個對象遮晚,這個被稱作為浮動垃圾,解決方案就是等下次GC的時候再清理拦止,這次掃不干凈就等你媽下次打掃衛(wèi)生的時候再清理就行了县遣。
與此相反,如果把存活對象標(biāo)記成需要清理汹族,那么就有點麻煩了萧求,這樣你的程序就該出問題了。
所以經(jīng)過研究表明顶瞒,只有同時滿足兩個條件才會發(fā)生這種對象消失的問題:
- 插入了一條或者多條黑色到白色對象的引用
- 刪除了全部從灰色到白色對象的引用
那么夸政,針對這個問題也有兩種解決方案:增量更新和原始快照,如果對應(yīng)到垃圾回收器的話搁拙,CMS使用的是增量更新秒梳,而像G1則是使用原始快照。
思路就是既然要同時滿足箕速,那么我只需要破壞其中一個條件那么不就可以了嗎酪碘?
所以,先看上面我們的例子中的一個場景盐茎,假設(shè)A掃描完兴垦,剛好C成為灰色,此時C->D的引用刪除字柠,同時A->D新增了引用(同時滿足兩個條件了吧)探越,這樣本來按照順序接下來D應(yīng)該會變成黑色(黑色對象不應(yīng)該被清理),但是由于C->D沒有引用了窑业,A已經(jīng)成為了黑色對象钦幔,他不會再被重新掃描了,所以即便新增了A->D的引用常柄,D也只能成為白色對象鲤氢,最終被無情地清理。
[圖片上傳失敗...(image-ede014-1625575081690)]
增量更新解決方案就是西潘,他會把這些新插入的引用記錄下來卷玉,掃描結(jié)束之后,再以黑色對象為根重新掃描一次喷市。這樣看起來不就是增量更新嗎相种?新插入的記錄再掃一次!
原始快照則是去破壞第二個條件品姓,他把這個要刪除的引用記錄下來寝并,掃描結(jié)束之后,以灰色對象為根重新掃描一次缭黔。所以就像是快照一樣食茎,不管你刪沒刪,其實最終還是會按照之前的關(guān)系重新來一次馏谨。