深入理解Java虛擬機(jī)(二)Java垃圾回收看這篇就夠了埂陆!

引用《深入理解Java虛擬機(jī)》書里的一句話:
Java與C++之間有一堵由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的“高墻”苛白,墻外面的人想進(jìn)去,墻里面的人卻想出來焚虱。

概述

上一篇文章深入理解Java虛擬機(jī)(一)java運(yùn)行時(shí)數(shù)據(jù)區(qū)域中购裙,講到程序計(jì)數(shù)器、虛擬機(jī)棧鹃栽、本地方法棧的生滅都隨線程的生命周期躏率,也就是內(nèi)存的分配和回收都有確定性。而Java堆和方法區(qū)都為線程共享,具有不確定性薇芝,這兩個(gè)區(qū)域如何回收也是垃圾收集器所關(guān)注的蓬抄。

對(duì)象已死?

在進(jìn)行垃圾收集之前夯到,首先要判斷哪些對(duì)象是存活的倡鲸,哪些對(duì)象需要回收。

引用計(jì)數(shù)算法

在對(duì)象中添加一個(gè)引用計(jì)數(shù)器黄娘,當(dāng)有地方引用它時(shí),計(jì)數(shù)器就加一克滴,當(dāng)引用失效時(shí)逼争,計(jì)數(shù)器就減一。如果計(jì)數(shù)器為零劝赔,代表對(duì)象不可能再被使用了誓焦。這種算法的缺點(diǎn)是,很難解決循環(huán)引用的問題着帽,Java虛擬機(jī)不使用這種算法杂伟。

可達(dá)性分析算法

可達(dá)性分析算法的基本思路是,以一系列稱為GC Roots的根對(duì)象開始仍翰,根據(jù)引用關(guān)系向下搜索赫粥,搜索走過的路徑稱為引用鏈。如果某個(gè)對(duì)象到GC Roots沒有引用鏈相連接予借,也就是說從GC Roots到這個(gè)對(duì)象不可達(dá)越平,說明對(duì)象不可能再被使用了。固定可作為GC Roots的對(duì)象包含以下幾種:

  • 在虛擬機(jī)棧(棧幀中的本地變量表)引用的對(duì)象灵迫,例如各個(gè)線程被調(diào)用的方法堆棧中使用的參數(shù)秦叛、局部變量、臨時(shí)變量等

  • 在方法區(qū)中類靜態(tài)屬性引用的對(duì)象瀑粥,例如Java類的引用類型靜態(tài)變量

  • 在方法區(qū)中常量引用的對(duì)象挣跋,例如字符串常量引用的對(duì)象

  • 本地方法棧中JNI(也就是Native方法)引用的對(duì)象

  • 所有被同步鎖(Synchronized關(guān)鍵字)引用的對(duì)象

  • Java虛擬機(jī)的內(nèi)部引用,如基本數(shù)據(jù)類型對(duì)于的Class對(duì)象狞换、常駐的異常對(duì)象避咆、系統(tǒng)類加載器

  • 映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)修噪、本地代碼緩存等牌借。

垃圾收集算法

標(biāo)記-復(fù)制算法

將可用內(nèi)存平均劃分為兩塊,每次只使用其中一塊割按。當(dāng)這一塊內(nèi)存用完時(shí)膨报,就將存活的對(duì)象復(fù)制到另一塊,再清理它。

這種算法的優(yōu)點(diǎn)是簡(jiǎn)單高效现柠,但如果多數(shù)對(duì)象都是存活的院领,那么會(huì)產(chǎn)生大量的復(fù)制開銷,而且由于內(nèi)存空間等分為兩塊够吩,對(duì)空間的使用率不高比然。

標(biāo)記-清除算法

首先標(biāo)記出需要回收的對(duì)象,標(biāo)記完成后周循,對(duì)所有標(biāo)記的對(duì)象統(tǒng)一回收强法。或者標(biāo)記存活對(duì)象湾笛,再統(tǒng)一回收未被標(biāo)記的對(duì)象饮怯。

這種算法的缺點(diǎn)是執(zhí)行效率不穩(wěn)定,如果有大量對(duì)象是要被標(biāo)記的嚎研,那么標(biāo)記和清除的效率都會(huì)因?qū)ο髷?shù)量的增長(zhǎng)而降低蓖墅。其次,它會(huì)造成內(nèi)存空間的碎片化临扮,不利于后續(xù)大對(duì)象的分配论矾。

標(biāo)記-整理算法

標(biāo)記-整理算法的標(biāo)記過程,和標(biāo)記-清除算法一樣杆勇,但是在進(jìn)行回收時(shí)贪壳,先將所有存活對(duì)象向內(nèi)存的一端移動(dòng),再進(jìn)行垃圾收集蚜退,這樣就保證了內(nèi)存空間使用的連續(xù)性寥袭。它的缺點(diǎn)便是移動(dòng)對(duì)象比較耗時(shí),需要暫停用戶應(yīng)用程序关霸。

因此传黄,移動(dòng)對(duì)象與否具有兩面性,不移動(dòng)則會(huì)帶來內(nèi)存空間碎片化队寇,但也可以在因碎片導(dǎo)致的對(duì)象無法分配時(shí)膘掰,再進(jìn)行整理;而移動(dòng)對(duì)象雖然會(huì)降低垃圾回收效率佳遣,但涉及到分配大對(duì)象時(shí)更劃算识埋。

分代收集理論

虛擬機(jī)通常將Java堆分為新生代和老年代。

新生代

新生代的目標(biāo)是盡可能快速的回收生命周期比較短的對(duì)象零渐,通常來說窒舟,新創(chuàng)建的對(duì)象會(huì)在新生代中。通常新生代還會(huì)細(xì)分為Eden區(qū)诵盼、Survivor 0區(qū)惠豺、Survivor 1區(qū)(8:1:1的比例)银还。當(dāng)Eden區(qū)空間不足時(shí),會(huì)發(fā)生一次Minor GC洁墙。此時(shí)會(huì)將Eden區(qū)的存活對(duì)象復(fù)制到Survivor 0區(qū)蛹疯,再清空Eden區(qū)。當(dāng)Eden區(qū)再次填滿時(shí)热监,同時(shí)回收Eden區(qū)和Survivor 0區(qū)捺弦,將存活對(duì)象放入Survivor 1區(qū),再清理另外兩個(gè)區(qū)域孝扛。也就是說列吼,必定會(huì)有一個(gè)Survivor區(qū)保持為空,供復(fù)制時(shí)使用苦始。但如果某個(gè)Survivor區(qū)被填滿寞钥,且有對(duì)象未復(fù)制完成,或者經(jīng)歷N次垃圾回收(-XX:MaxTenuringThreshold參數(shù)設(shè)置)后依然存活的對(duì)象盈简,會(huì)被放入老年代中。如果設(shè)置了 - XX:PretenureSizeThreshold 參數(shù)太示,超過這個(gè)大小的對(duì)象會(huì)直接進(jìn)入老年代柠贤,為的是避免大對(duì)象在新生代中來回復(fù)制。

老年代

老年代一般存放生命周期較長(zhǎng)的對(duì)象类缤,通常在新生代經(jīng)歷N次垃圾回收后依然存活的對(duì)象臼勉,會(huì)被放到老年代中。老年代的收集稱為Major GC餐弱。

其它回收方式

  • Mixed GC:只有G1收集器有這種方式宴霸,回收整個(gè)新生代以及部分老年代。

  • Full GC:對(duì)整個(gè)堆和方法區(qū)進(jìn)行垃圾回收

HotSpot的算法實(shí)現(xiàn)細(xì)節(jié)

根節(jié)點(diǎn)枚舉

現(xiàn)在Java應(yīng)用越來越龐大膏蚓,類废岂、常量等數(shù)量就像恒河沙數(shù)帜矾,從GC Roots集合高效查找引用鏈并非易事。

雖然可達(dá)性分析算法耗時(shí)最長(zhǎng)的查找引用鏈的過程,已經(jīng)可以做到與用戶線程一起并發(fā)该贾。

但迄今為止的所有收集器在根節(jié)點(diǎn)枚舉時(shí),都必須暫停用戶線程徘意,也就是"Stop The World"懂昂。根節(jié)點(diǎn)枚舉是導(dǎo)致垃圾收集過程必須停頓所有用戶線程的一個(gè)重要原因。

目前主流Java虛擬機(jī)使用的都是準(zhǔn)確式垃圾收集狂魔,所以當(dāng)用戶線程停頓下來之后蒜埋,其實(shí)并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文(例如棧幀中的本地變量表)和全局引用(例如常量、類靜態(tài)屬性)的位置最楷,有專門的地方來存放對(duì)象引用的整份。

在HotSpot中待错,使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個(gè)目的。一旦類加載動(dòng)作完成皂林,HotSpot就會(huì)把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算處理朗鸠,在即時(shí)編譯過程中,也會(huì)在特定位置記錄下棧里和寄存器里哪些位置是引用础倍。

安全點(diǎn)

如果每條指令都生成對(duì)應(yīng)的OopMap烛占,那么將需要大量額外的存儲(chǔ)空間。HotSpot虛擬機(jī)也沒有這么做沟启,只是在特定位置記錄這些信息忆家,這些位置稱為“安全點(diǎn)”。安全點(diǎn)的設(shè)定強(qiáng)制要求代碼指令流必須執(zhí)行到安全點(diǎn)后才能暫停開始垃圾收集德迹,而非在任意位置都能停頓下來芽卿。

總結(jié)來說,安全點(diǎn)是用來解決如何停頓用戶線程胳搞,讓虛擬機(jī)進(jìn)入垃圾回收狀態(tài)卸例。

在垃圾收集發(fā)生時(shí)讓所有線程都跑到最近的安全點(diǎn),然后停頓下來肌毅,有兩種方案:

  • 搶先式中斷:在垃圾收集發(fā)生時(shí)筷转,先把所有用戶線程全部中斷,如果有線程沒有到達(dá)安全點(diǎn)悬而,則讓它繼續(xù)執(zhí)行到安全點(diǎn)上呜舒。但是虛擬機(jī)一般不用這種方式。

  • 主動(dòng)式中斷:當(dāng)垃圾收集發(fā)生時(shí)笨奠,不直接對(duì)線程操作袭蝗,而是設(shè)定一個(gè)標(biāo)志位也就是安全點(diǎn),各個(gè)線程執(zhí)行過程中不斷主動(dòng)輪詢般婆,一旦發(fā)現(xiàn)中斷標(biāo)志為真到腥,就主動(dòng)掛起。

安全區(qū)域

當(dāng)用戶線程處于Sleep狀態(tài)或者Blocked狀態(tài)蔚袍,將無法響應(yīng)虛擬機(jī)的中斷請(qǐng)求左电,也不無法自己執(zhí)行到安全點(diǎn)。這種情況使用安全區(qū)域來處理页响。

安全區(qū)域是指能夠確保在某一段代碼片段之中篓足,引用關(guān)系不會(huì)發(fā)生變化,因此這塊區(qū)域內(nèi)任意地方開始垃圾回收都是安全的闰蚕。

記憶集與卡表

為解決跨代引用所帶來的問題栈拖,垃圾收集器在新生代中建立了名為記憶集(Remembered Set)的數(shù)據(jù)結(jié)構(gòu),用以避免把整個(gè)老年代加進(jìn)GC Roots掃描范圍没陡。(整個(gè)老年代加入掃描將嚴(yán)重影響性能)

記憶集是一種用于記錄從非收集區(qū)域指向收集區(qū)域的指針集合的抽象數(shù)據(jù)結(jié)構(gòu)涩哟。底層用非收集區(qū)域中所有含跨代引用的對(duì)象數(shù)組來實(shí)現(xiàn)這個(gè)數(shù)據(jù)結(jié)構(gòu)索赏。

有三種記錄精度供選擇:

  • 字長(zhǎng)精度:每個(gè)記錄精確到一個(gè)機(jī)器字長(zhǎng)

  • 對(duì)象精度:每個(gè)記錄精確到一個(gè)對(duì)象,該對(duì)象里有字段含有跨代指針

  • 卡精度:每個(gè)記錄精確到一個(gè)內(nèi)存區(qū)域贴彼,該區(qū)域內(nèi)含有的對(duì)象含有跨代指針

目前最常用第三種方式潜腻,也就是用“卡表”(Card Table)來實(shí)現(xiàn)記憶集。

寫屏障

寫屏障用來維護(hù)卡表元素的狀態(tài)器仗,也就是卡頁對(duì)應(yīng)的內(nèi)存區(qū)域中是否存在跨代引用融涣。

寫屏障的實(shí)現(xiàn)原理是在虛擬機(jī)層面對(duì)“引用類型字段賦值”這個(gè)動(dòng)作的AOP切面,賦值時(shí)會(huì)產(chǎn)生一個(gè)環(huán)形(Around)通知精钮,供程序執(zhí)行額外動(dòng)作威鹿。

經(jīng)典的垃圾收集器

圖片來自《深入理解Java虛擬機(jī)第三版》

圖中展示了七種作用于不同分代的收集器,如果兩個(gè)收集器之間存在連線轨香,就說明它們可以搭配使用忽你,圖中收集器所處的區(qū)域,則表示它是屬于新生代收集器抑或是老年代收集器臂容。

JDK9標(biāo)識(shí)的線條科雳,表示在JDK9時(shí)取消了搭配使用。

下圖對(duì)垃圾收集器做了歸類和總結(jié)脓杉,本文僅介紹CMS和G1收集器的算法原理糟秘。

CMS收集器

CMS(Concurrent Mark Sweep)收集器以獲取最短停頓時(shí)間為目標(biāo),它基于標(biāo)記-清除算法丽已,整個(gè)過程分為四個(gè)步驟:

  • 初始標(biāo)記:僅標(biāo)記GC Roots能夠直接關(guān)聯(lián)到的對(duì)象蚌堵,速度很快买决,需要停止用戶線程

  • 并發(fā)標(biāo)記:就是從GC Roots直接關(guān)聯(lián)的對(duì)象開始遍歷整個(gè)對(duì)象樹沛婴,不需要停止用戶線程

  • 重新標(biāo)記:修正在并發(fā)標(biāo)記期間,由于用戶線程繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那部分對(duì)象的標(biāo)記記錄督赤,需要停止用戶線程

  • 并發(fā)清除:清理標(biāo)記階段判斷為已死亡的對(duì)象嘁灯,由于不需要移動(dòng)存活對(duì)象,因此不需要停止用戶線程

圖片來自《深入理解Java虛擬機(jī)第三版》

說明
并行(Parallel):并行描述的是多條垃圾收集器線程之間的關(guān)系躲舌,說明同一時(shí)間有多條這樣的線程在協(xié)同工作丑婿,通常默認(rèn)此時(shí)用戶線程是處于等待狀態(tài)。
并發(fā)(Concurrent):并發(fā)描述的是垃圾收集器線程與用戶線程之間的關(guān)系没卸,說明同一時(shí)間垃圾收集器線程與用戶線程都在運(yùn)行羹奉。

G1收集器

G1收集器開創(chuàng)了面向局部收集的設(shè)計(jì)思路和基于Region的內(nèi)存布局形式,它把連續(xù)的Java堆劃分為大小相等的獨(dú)立區(qū)域(Region)约计,每一個(gè)Region都可以根據(jù)需要扮演新生代的Eden空間诀拭,Survivor空間,或者老年代空間煤蚌。還有一類特殊的Humongous區(qū)域耕挨,專門用來存儲(chǔ)大對(duì)象细卧。G1為每個(gè)Region分別兩個(gè)名為TAMS的指針,因?yàn)榛厥者^程與用戶線程會(huì)并發(fā)執(zhí)行筒占,期間可能有新對(duì)象創(chuàng)建贪庙,新分別的對(duì)象地址必須在兩個(gè)指針位置以上。

圖片來自《深入理解Java虛擬機(jī)第三版》

G1收集器整體上是用標(biāo)記-整理算法來實(shí)現(xiàn)的翰苫,但從局部來看(兩個(gè)Region之間)是基于標(biāo)記-復(fù)制算法來實(shí)現(xiàn)的止邮,具體分為四個(gè)步驟:

  • 初始標(biāo)記:僅標(biāo)記GC Roots能直接關(guān)聯(lián)的對(duì)象,并移動(dòng)TAMS指針革骨,保證下一階段用戶線程執(zhí)行時(shí)能正確創(chuàng)建對(duì)象农尖。此階段用戶線程需要停頓,但耗時(shí)很短

  • 并發(fā)標(biāo)記:從GC Roots開始遞歸掃描對(duì)象圖良哲,找到要回收的對(duì)象盛卡,這個(gè)過程耗時(shí)比較長(zhǎng),但可以與用戶線程并發(fā)執(zhí)行

  • 最終標(biāo)記:短暫暫停用戶線程筑凫,處理上一步中引用發(fā)生改變的對(duì)象的SATB記錄

  • 篩選回收:更新Region的統(tǒng)計(jì)數(shù)據(jù)滑沧,對(duì)Region的回收價(jià)值和成本進(jìn)行排序,再根據(jù)用戶配置的停頓時(shí)間巍实,將決定回收的Region中的存活對(duì)象滓技,復(fù)制到空的Region中。由于此過程涉及對(duì)象的移動(dòng)棚潦,因此需要暫停用戶線程令漂。由此也可以看出它并非只關(guān)注低延遲,還關(guān)注吞吐量丸边。

圖片來自《深入理解Java虛擬機(jī)第三版》

本文主要為閱讀《深入理解Java虛擬機(jī) 第三版》所做的筆記總結(jié)叠必,如有錯(cuò)漏歡迎支持,謝謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末妹窖,一起剝皮案震驚了整個(gè)濱河市纬朝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌骄呼,老刑警劉巖共苛,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蜓萄,居然都是意外死亡隅茎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門嫉沽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辟犀,“玉大人,你說我怎么就攤上這事耻蛇∽俚牛” “怎么了胞此?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)跃捣。 經(jīng)常有香客問我漱牵,道長(zhǎng),這世上最難降的妖魔是什么疚漆? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任酣胀,我火速辦了婚禮,結(jié)果婚禮上娶聘,老公的妹妹穿的比我還像新娘闻镶。我一直安慰自己,他們只是感情好丸升,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布铆农。 她就那樣靜靜地躺著,像睡著了一般狡耻。 火紅的嫁衣襯著肌膚如雪墩剖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天夷狰,我揣著相機(jī)與錄音岭皂,去河邊找鬼。 笑死沼头,一個(gè)胖子當(dāng)著我的面吹牛爷绘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播进倍,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼土至,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了背捌?” 一聲冷哼從身側(cè)響起毙籽,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤洞斯,失蹤者是張志新(化名)和其女友劉穎毡庆,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烙如,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡么抗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了亚铁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝇刀。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖徘溢,靈堂內(nèi)的尸體忽然破棺而出吞琐,到底是詐尸還是另有隱情捆探,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布站粟,位于F島的核電站黍图,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏奴烙。R本人自食惡果不足惜助被,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望切诀。 院中可真熱鬧揩环,春花似錦、人聲如沸幅虑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽倒庵。三九已至吨枉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哄芜,已是汗流浹背貌亭。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留认臊,地道東北人圃庭。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像失晴,于是被迫代替她去往敵國(guó)和親剧腻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345