自底向上解析垃圾收集器

  • 引子

Java作為一個安全語言揉燃,不會向程序員提供內(nèi)存管理api迎卤,而是把這個任務交給垃圾收集器墩弯,程序員不需要關心對象分配在哪它匕,也不需要關心對象如何分配/回收內(nèi)存展融。本文會詳細介紹這一機制。


對象什么時候死?

傳統(tǒng)靜態(tài)編譯器會使用『活性分析』算法來確定一個對象的生存期:如果一個變量持有一個值豫柬,那么這個變量的生存期以『對這個變量的第一次寫』開始告希,以『對這個變量的最后一次讀結束』,在這期間烧给,該變量都是活躍的燕偶。

這種方式確實可以確定變量的生存期,但是只能局限于某個方法內(nèi)的局部變量创夜,因為java是個動態(tài)性很強的語言杭跪,擁有虛方法(多態(tài))和異常拋出這種運行期才能明確的過程跳轉,是的驰吓,根本無法靜態(tài)分析出下個代碼塊是啥涧尿,又怎么能確定變量的生存期呢(比如發(fā)生參數(shù)傳遞的方法跳轉)。

但『活性分析』也不是一無是處檬贰,編譯器為保證程序的局部性姑廉,會對方法進行『逃逸分析』,分析出方法的局部變量翁涤,輔助『寄存器分配』這樣的優(yōu)化桥言。

ps:『逃逸分析』不屬于GC的范疇,他只是輔助runtime做一些優(yōu)化葵礼。逃逸分析由-XX:+DoEscapeAnalysis參數(shù)控制号阿,jdk1.8默認開啟。
class 活性分析案例1 {

   int a = 1;
   
   void 優(yōu)化前(){
        while(1w次){
            a++;
        }
        print(a);
   }
   
   void 優(yōu)化后(){
        //棧上分配
        int temp = a;
        while(1w次){
            temp++;
        }
        print(temp);
   } 
}

class 活性分析案例2 {

   void test(){
        for(int i = 0; i < 5000000; i++){
            createObject();
        }
   }
   
   public static void createObject(){
        //加鎖
        synchronized (new Object()){

        }
   }
}
案例1:

局部優(yōu)化鸳粉。

案例2:

第一個點:逃逸分析后對象直接進行『標量替換』扔涧,使test方法結束后直接回收對象,減少垃圾收集器壓力。
第二個點:逃逸分析后進行鎖消除枯夜,直接忽略synchronized關鍵字弯汰。

ps: 標量替換:Java的原始類型無法被分解可以被看作『標量』,引用(指針)也無法被分解可以被看作『標量』湖雹。而一個對象一定是標量的聚合體咏闪,把對象分解為標量后可以被編譯器優(yōu)化,在棧/寄存器上分配摔吏。
注意鸽嫂!Java對象在實際的JVM實現(xiàn)中可能在GC堆上分配空間,也可能在棧上分配空間舔腾,也可能完全就消失了溪胶。

由此可以得出,一個對象什么時候該被回收稳诚,這個時間點難以精準確定哗脖,因為這需要預測程序未來的行為。 但是可以用對象是否可達來近似對象是否已死扳还,當發(fā)現(xiàn)一個對象不可達的時候才避,這個對象肯定已經(jīng)死過了(可能死好一會了)。雖然回收的時間肯定會晚于對象死的時間氨距,但這絕對是及時性分析復雜度的一個合理的妥協(xié)桑逝!下面會詳細介紹幾種『可達算法』。


引用計數(shù)(RC)

判斷一個對象是否可達俏让,很直觀的一種方式引用計數(shù)楞遏,就是把對象的引用數(shù)目用計數(shù)器維護起來,當對象被
引用時首昔,就遞增計數(shù)器寡喝;當引用被覆蓋時就遞減計數(shù)器,當計數(shù)器減少至0的時候勒奇,這個對象就需要被回收预鬓,回收
當前對象時需要減少當前對象引用的所有對象的計數(shù)器。

比如S對象引用了A赊颠、B格二、C,那么S被回收時竣蹦,A顶猜、B、C三個對象的計數(shù)器要遞減痘括,這個過程會被持續(xù)傳遞驶兜!

下面介紹一個簡單的引用計數(shù)實現(xiàn):

首先是引用計數(shù)指令的抽象,如下:
       
       incRC    RC遞增
       decRC    RC遞減
       testRC   判斷RC是否降為0,如果是抄淑,進行傳遞回收

RC指令需要編譯器插樁到代碼中,比如:

1. 對象obj1的一個引用加載到棧上驰后,比如Ref ref1 = obj1 肆资,需要插入incRC obj1
2. ref1從指向obj1切換為obj2灶芝,比如Ref ref1 = obj2郑原,需要插入incRC obj2decRC obj2夜涕、testRC obj2犯犁。
3. ref1指向obj1,ref1指向切換為NULL女器,然后ref2指向obj1酸役,ref1指向切換為NULL。需要插入incRC obj1驾胆、decRC obj1涣澡、testRC obj1incRC obj2丧诺、decRC obj2入桂、testRC obj2

這種RC存在的問題:

1. 可以發(fā)現(xiàn),RC可能會帶來巨大的運行時開銷驳阎。而這些開銷很多都是冗余的抗愁,比如上述的案例3,這一連串的incRc呵晚、decRC蜘腌、testRC其實完全可以替換為一個testRC。
2. 多線程應用程序中更新RC需要原子指令劣纲,原子指令都是代價昂貴的
3. A引用B逢捺,B引用A,這就形成了循環(huán)引用癞季,然而引用計數(shù)沒有維護依賴網(wǎng)劫瞳,所以根本無法檢測出是否循環(huán)引用。雖然沒有其他變量引用他們绷柒,但A和B的引用計數(shù)始終無法歸零志于,產(chǎn)生了A和B這倆漂浮垃圾。


對象追蹤

上面提到過RC主要問題是沒有維護『依賴網(wǎng)』废睦,對象追蹤可以解決這一問題伺绽,主要分為兩步:

    1. 根集枚舉。確定執(zhí)行上下文(棧、寄存器奈应、全局變量)中的所有槽位澜掩。
    1. 堆追蹤。遍歷對象鄰接圖,直到所有對象都被訪問到杖挣。

通常情況下肩榕,對象追蹤不能在用戶程序活躍的時候進行,因為這個時候執(zhí)行上下文對象圖都在持續(xù)變化惩妇。這個時候
對象追蹤用戶程序執(zhí)行是競爭的關系株汉。因此GC開始對象追蹤時候需要暫停用戶程序,也叫STW(stop-the-world)歌殃。

ps:并不是所有的語言都能進行根集枚舉乔妈,非安全語言可能會讓編譯器很迷惑,比如在整數(shù)變量中保存引用氓皱。


分代回收

有這么一個假設"大部分對象都會die young路召,沒有die young的對象生命期都會很長",這個假設來源于很多應用的行為分析匀泊。
基于這個假設:那么可以讓對象都在young gen中創(chuàng)建优训,然后對其頻繁回收,由于大部分對象都會die young各聘,所以young gen非常適合copy算法揣非,減少碎片的同時還能提高GC效率。

Hotsport VM把young gen分為了eden躲因、survival兩個區(qū)域早敬,其中survival又分成了from、to兩個區(qū)域大脉。一個對象的young gen之旅如下:

young gen
    1. 對象先在eden區(qū)分配
    1. eden達到閾值觸發(fā)young gc搞监,進行復制-清除,將幸存對象copy至to
    1. 回收from區(qū)镰矿,同樣復制-清除琐驴,copy至to
    1. to變?yōu)閒rom,from變?yōu)閠o
    1. 以此輪回下去秤标。這個過程中對象每次從from copy到to绝淡,年齡都會+1
    1. 對象在某次copy中年齡達到了閾值,copy到old gen
ps: 為什么要把survival分成兩塊呢苍姜?

首先是為什么需要survival區(qū)域牢酵?這源于young gen的對象年齡設計,每次對象年齡增長總要有個區(qū)域用來復制(總不能直接復制到old gen吧衙猪,這樣年齡就沒意義了)馍乙,所以就有了survival布近。
為什么要分成兩塊呢?首先要明白復制算法一定要有個區(qū)域用于復制丝格。如果只有一塊撑瞧,那么在survival就只能使用標記清除算法,容易產(chǎn)生內(nèi)存碎片铁追,違背了young gen設計的初衷季蚂。也有人說,那為什么不能再來一次內(nèi)存整理呢琅束?我猜測是內(nèi)存整理的開銷遠遠大于復制,索性浪費一小塊內(nèi)存用來復制反而更實在算谈。

下面是幾種基本GC算法的性能對比:
mark-sweep mark-compact copy
速度 中等 最慢 最快
空間開銷 少(但會堆積碎片) 少(不堆積碎片) 通常需要活對象的2倍大猩鳌(不堆積碎片)
移動?

CMS垃圾收集器

CMS(Concurrent Mark Sweep)收集器是一個以『低延遲』為特點的垃圾收集器然眼,適合主流的B/S系統(tǒng)艾船。
從命名也能看出來,CMS能和用戶程序并發(fā)執(zhí)行高每,他的回收區(qū)域為「old gen」屿岂,其回收過程如下:

    1. 初始標記:根集枚舉,列舉所有GC ROOT能直接關聯(lián)到的對象鲸匿。由于CMS并不是whole heap垃圾收集器爷怀,所以CMS回收old gen時,必須把young gen算作是ROOT带欢,那么CMS的GC ROOT包含棧运授、寄存器、全局變量以及young gen乔煞。(同樣的道理吁朦,young gen GC也要把old gen作為GC ROOT)
    1. 并發(fā)標記:開始對象追蹤,標記所有可達的對象渡贾,這個過程與用戶線程并發(fā)執(zhí)行
    1. 重新標記:重新掃描2步中「write barrier」維護的「記憶集」以及GC ROOT逗宜,該階段需要STW
    1. 并發(fā)清除:清除對象

并發(fā)標記: 既然CMS的特點是低延遲,他的STW時間要足夠小空骚。為了達到這一目的:GC線程(回收器)用戶線程(修改器)應盡量并發(fā)纺讲。而在并發(fā)過程中,回收器的標記階段需要遍歷對象鄰接圖府怯,這個時候對象鄰接圖是在變化的刻诊,大致有兩種變化:

    1. 修改器修改對象的引用字段,這可能會導致某個對象死亡牺丙。假設在活躍對象集L中则涯,標記結束后仍然可達的對象為集合$L复局,那么有$LL的子集。
    1. 修改器創(chuàng)建了新的對象粟判,這些對象可能一直可達亿昏,也有可能死亡。假設標記階段創(chuàng)建的新的對象為集合N档礁,標記結束后角钩,他們中仍然可達的對象為集合$N,那么有 $NN的子集呻澜。
并發(fā)標記.png

可以得出結論递礼,經(jīng)過并發(fā)標記后,真正活躍的對象集為 $L+$N 羹幸,且 L + N ? $L +$N 脊髓。

為了保證正確性,并發(fā)標記期間不能丟失任何活躍的對象(不能錯殺)栅受,那么可以用所有活躍對象的超集 L + N 将硝,并發(fā)標記結束后,再掃描一遍屏镊,不就可以保證正確性了么依疼,這種方式叫做原始快照,也就是SATB(Snapshot-At-The-Beginning)而芥。

還有另外一個思路律罢,并發(fā)標記結束后,找到真正的活躍對象 $L + $N蔚出,這種方式叫做增量更新弟翘,也就是INC( Incremental Update),而CMS采用的就是 INC骄酗。

不管是SATB還是INC稀余,都需要一個維護引用變更的機制,這個機制就是 寫屏障趋翻。下面提供INC的偽代碼:

//src:某個對象睛琳,slot:某個槽,new_ref;新引用
write_barrier_ref(Object* src,object** slot,Object* new_ref){

     *slot = new_ref();

     if( is_marked(src) ){
          remember(new_ref());
     }
}
代碼解釋:

如果當前對象已經(jīng)被標記了踏烙,對這個對象的某個槽賦予新引用時师骗,write_barrier需要記憶new_ref。如果當前對象沒有被標記讨惩,write_barrier不需要記憶任何東西辟癌,等當前對象被標記的時候自然會去掃描他的new_ref

案例:
并發(fā)標記問題.png

在并發(fā)標記的過程中荐捻,a黍少、b寡夹、A都已經(jīng)被標記,這個時候修改器斷開AB之間的引用厂置,讓b引用B菩掏,如果write_barrier沒有記憶這個引用變更,對象B就會被錯殺昵济。既然write_barrier記憶了會被錯殺的對象智绸,那么應該在并發(fā)標記之后來一次重新標記,保證正確性访忿!重新標記稍后介紹瞧栗。

標記過程中的三色標記: 假設對象圖為 G,其中 $G 都已經(jīng)被掃描過海铆,那么 (G - $G) 部分還沒被掃描沼溜。假如這個時候修改器在修改對象圖,那么會產(chǎn)生兩種效果:

    1. $G 的一部分對象死亡游添,但作為漂浮垃圾保留。(write_barrier只處理新引用)
    1. (G-$G)的一部分對象失去連接通熄,只與$G 中的對象連接唆涝。

第一種效果不會導致正確性的問題,而第二種則會導致對象被錯殺唇辨。實際上廊酣,CMS的標記過程并不是非黑即白,還有一個
赏枚。下面用三色標記術語來解釋上述問題亡驰。

三色標記.png

掃描過的對象 $G 為黑色。未掃描的對象(G - $G) 為白色饿幅》踩瑁灰色為掃描過,但slot沒有掃描完全栗恩,如圖中的灰色對象透乾。灰色對象是已經(jīng)確定可達磕秤,但是他引用的對象還未完全確定乳乌。一個對象不會直接從黑色變成白色。

如圖所示市咆,在并發(fā)標記過程中汉操,修改器修改引用,使黑色對象直接引用白色對象蒙兰,如果沒有STAB或者INC寫屏障磷瘤,這個白色對象會被錯殺芒篷,因為掃描過的黑色對象不會再被掃描。

按照這個思路膀斋,在三色標記的過程中梭伐,有兩種情況會導致對象被錯殺:

1. 原本在(G - $G)中的可達對象,現(xiàn)在只有從$G中掃描過的對象到達仰担。這種情況會被INC寫屏障追蹤糊识。

2. 原本在(G - $G)中的可達對象,現(xiàn)在只有從非堆位置到達摔蓝,如棧赂苗、寄存器等GC根節(jié)點。這種情況不會被INC寫屏障追蹤贮尉!

那么從三色標記的角度拌滋,解釋了為什么需要STAB、INC寫屏障猜谚。也可以得出败砂,重新標記是必須的,而且需要掃描「記憶集(寫屏障)」和 「GC根節(jié)點」魏铅。還有幾個點要注意昌犹!

  1. CMS的GC根節(jié)點包含了整個young gen。可能會有這樣的疑問览芳,為什么young gen不去維護remembered set斜姥,而是在CMS標記時,掃描整個young gen沧竟?使用remembered set的初衷就是為了減少掃描的非收集區(qū)域大小铸敏,只掃描有變化的部分,然而young gen的引用變更實在是太頻繁了悟泵,給young gen維護remembered set會產(chǎn)生很大的開銷杈笔,而且影響吞吐量!索性就把整個young gen都作為GC Root(掃描但不被收集)魁袜。
    所以在重新標記階段桩撮,CMS會全堆掃描,所以young gen對象數(shù)目會直接影響重新標記階段的STW時長峰弹,如果觀察GC日志發(fā)現(xiàn)重新標記階段耗時長且young gen使用率高店量,可以使用參數(shù)CMSScavengeBeforeRemark 強制讓CMS在在重新標記前進行一次并發(fā)預清理(Minor GC) 。當然不加這個參數(shù)鞠呈,CMS也會進行并發(fā)預清理融师,只是需要等到下次Minor GC的到來,為了避免這個階段沒有等到Minor GC而陷入無限等待蚁吝,CMS提供了CMSMaxAbortablePrecleanTime參數(shù)來配置等待時間旱爆,默認為5秒舀射。

注:astore1,等修改棧指針的字節(jié)碼不會引發(fā)write barrier怀伦,能引發(fā)write barrier的字節(jié)碼只有putfield脆烟,putstatic和aastore。年輕代GC就是Minor GC

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末房待,一起剝皮案震驚了整個濱河市邢羔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌桑孩,老刑警劉巖拜鹤,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異流椒,居然都是意外死亡敏簿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門宣虾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惯裕,“玉大人,你說我怎么就攤上這事绣硝∏岵” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我剪决,道長歧匈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任王带,我火速辦了婚禮淑蔚,結果婚禮上,老公的妹妹穿的比我還像新娘愕撰。我一直安慰自己刹衫,他們只是感情好,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布搞挣。 她就那樣靜靜地躺著带迟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪囱桨。 梳的紋絲不亂的頭發(fā)上仓犬,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音舍肠,去河邊找鬼搀继。 笑死窘面,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的叽躯。 我是一名探鬼主播财边,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼点骑!你這毒婦竟也來了酣难?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤畔况,失蹤者是張志新(化名)和其女友劉穎鲸鹦,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跷跪,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡馋嗜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吵瞻。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葛菇。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖橡羞,靈堂內(nèi)的尸體忽然破棺而出眯停,到底是詐尸還是另有隱情,我是刑警寧澤卿泽,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布莺债,位于F島的核電站,受9級特大地震影響签夭,放射性物質(zhì)發(fā)生泄漏齐邦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一第租、第九天 我趴在偏房一處隱蔽的房頂上張望措拇。 院中可真熱鬧,春花似錦慎宾、人聲如沸丐吓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽券犁。三九已至,卻和暖如春汹碱,著一層夾襖步出監(jiān)牢的瞬間族操,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留色难,地道東北人泼舱。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像枷莉,于是被迫代替她去往敵國和親娇昙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 目錄 一.背景 二.CMS垃圾收集器特性 三.CMS執(zhí)行步驟 四.CMS日志解釋(JDK1.8): 五.CMS參數(shù)...
    愛吃糖果閱讀 4,385評論 0 3
  • 一笤妙、新生代垃圾回收器的比較: 二冒掌、老年代垃圾回收器的比較: 四、CMS的特性 1蹲盘、CMS只會回收老年代和永久代的垃...
    紫雨杰閱讀 678評論 0 0
  • CMS是老年代垃圾收集器股毫,在收集過程中可以與用戶線程并發(fā)操作。它可以與Serial收集器和Parallel New...
    zhong0316閱讀 40,630評論 2 27
  • 垃圾回收主要是要解決3件事情: 那些內(nèi)存需要回收召衔? 如何回收铃诬? 什么時候回收? 術語解釋 并行/并發(fā) 并行(Par...
    xiaolyuh閱讀 312評論 0 3
  • 前言 上一篇我們介紹了對象在堆內(nèi)的內(nèi)存布局已經(jīng)占用空間的大小苍凛,同時分析了堆內(nèi)可以分為Young區(qū)和Old區(qū)趣席,而且Y...
    刀哥說Java閱讀 282評論 0 1