一、如何判定對象為垃圾對象
在堆里面存放著Java世界中幾乎所有的對象實例, 垃圾收集器在對堆進行回收前, 第一件事就是判斷哪些對象已死(可回收).
1. 引用計數(shù)法
在JDK1.2之前,使用的是引用計數(shù)器算法功蜓。 在對象中添加一個引用計數(shù)器,當(dāng)有地方引用這個對象的時候蕊梧,引用計數(shù)器的值就+1霞赫,當(dāng)引用失效的時候,計數(shù)器的值就-1肥矢,當(dāng)引用計數(shù)器被減為零的時候端衰,標(biāo)志著這個對象已經(jīng)沒有引用了叠洗,可以回收了!
問題:如果在A類中調(diào)用B類的方法旅东,B類中調(diào)用A類的方法灭抑,這樣當(dāng)其他所有的引用都消失了之后,A和B還有一個相互的引用抵代,也就是說兩個對象的引用計數(shù)器各為1腾节,而實際上這兩個對象都已經(jīng)沒有額外的引用,已經(jīng)是垃圾了荤牍。但是該算法并不會計算出該類型的垃圾案腺。
2. 可達(dá)性分析法
在主流商用語言(如Java、C#)的主流實現(xiàn)中, 都是通過可達(dá)性分析算法來判定對象是否存活的: 通過一系列的稱為 GC Roots 的對象作為起點, 然后向下搜索; 搜索所走過的路徑稱為引用鏈/Reference Chain, 當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連時, 即該對象不可達(dá), 也就說明此對象是不可用的, 如下圖:雖然E和F相互關(guān)聯(lián)康吵, 但它們到GC Roots是不可達(dá)的, 因此也會被判定為可回收的對象劈榨。
注: 即使在可達(dá)性分析算法中不可達(dá)的對象, VM也并不是馬上對其回收, 因為要真正宣告一個對象死亡, 至少要經(jīng)歷兩次標(biāo)記過程: 第一次是在可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈, 第二次是GC對在F-Queue執(zhí)行隊列中的對象進行的小規(guī)模標(biāo)記(對象需要覆蓋finalize()方法且沒被調(diào)用過).
在Java, 可作為GC Roots的對象包括:
- 方法區(qū): 類靜態(tài)屬性引用的對象;
- 方法區(qū): 常量引用的對象;
- 虛擬機棧(本地變量表)中引用的對象.
- 本地方法棧JNI(Native方法)中引用的對象。
二晦嵌、如何回收
回收策略
垃圾收集策略有分代收集和分區(qū)收集同辣。
分代收集算法
1. 標(biāo)記-清除算法(老年代)
該算法分為“標(biāo)記”和“清除”兩個階段: 首先標(biāo)記出所有需要回收的對象(可達(dá)性分析), 在標(biāo)記完成后統(tǒng)一清理掉所有被標(biāo)記的對象.
該算法會有兩個問題:
- 效率問題,標(biāo)記和清除效率不高惭载。
- 空間問題: 標(biāo)記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片, 空間碎片太多可能會導(dǎo)致在運行過程中需要分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集旱函。
所以它一般用于"垃圾不太多的區(qū)域,比如老年代"描滔。
2. 復(fù)制算法(新生代)
該算法的核心是將可用內(nèi)存按容量劃分為大小相等的兩塊, 每次只用其中一塊, 當(dāng)這一塊的內(nèi)存用完, 就將還存活的對象(非垃圾)復(fù)制到另外一塊上面, 然后把已使用過的內(nèi)存空間一次清理掉.
優(yōu)點:不用考慮碎片問題棒妨,方法簡單高效。 缺點:內(nèi)存浪費嚴(yán)重伴挚。
現(xiàn)代商用VM的新生代均采用復(fù)制算法, 但由于新生代中的98%的對象都是生存周期極短的, 因此并不需完全按照1∶1的比例劃分新生代空間, 而是將新生代劃分為一塊較大的Eden區(qū)和兩塊較小的Survivor區(qū)(HotSpot默認(rèn)Eden和Survivor的大小比例為8∶1), 每次只用Eden和其中一塊Survivor. 當(dāng)發(fā)生MinorGC時, 將Eden和Survivor中還存活著的對象一次性地拷貝到另外一塊Survivor上, 最后清理掉Eden和剛才用過的Survivor的空間. 當(dāng)Survivor空間不夠用(不足以保存尚存活的對象)時, 需要依賴?yán)夏甏M行空間分配擔(dān)保機制, 這部分內(nèi)存直接進入老年代靶衍。
復(fù)制算法的空間分配擔(dān)保: 在執(zhí)行Minor GC前, VM會首先檢查老年代是否有足夠的空間存放新生代尚存活對象, 由于新生代使用復(fù)制收集算法, 為了提升內(nèi)存利用率, 只使用了其中一個Survivor作為輪換備份, 因此當(dāng)出現(xiàn)大量對象在Minor GC后仍然存活的情況時, 就需要老年代進行分配擔(dān)保, 讓Survivor無法容納的對象直接進入老年代, 但前提是老年代需要有足夠的空間容納這些存活對象. 但存活對象的大小在實際完成GC前是無法明確知道的, 因此Minor GC前, VM會先首先檢查老年代連續(xù)空間是否大于新生代對象總大小或歷次晉升的平均大小, 如果條件成立, 則進行Minor GC, 否則進行Full GC(讓老年代騰出更多空間). 然而取歷次晉升的對象的平均大小也是有一定風(fēng)險的, 如果某次Minor GC存活后的對象突增,遠(yuǎn)遠(yuǎn)高于平均值的話,依然可能導(dǎo)致?lián)J?Handle Promotion Failure, 老年代也無法存放這些對象了), 此時就只好在失敗后重新發(fā)起一次Full GC(讓老年代騰出更多空間).
3. 標(biāo)記-整理算法(老年代)
標(biāo)記清除算法會產(chǎn)生內(nèi)存碎片問題, 而復(fù)制算法需要有額外的內(nèi)存擔(dān)痹痔浚空間, 于是針對老年代的特點, 又有了標(biāo)記整理算法. 標(biāo)記整理算法的標(biāo)記過程與標(biāo)記清除算法相同, 但后續(xù)步驟不再對可回收對象直接清理, 而是讓所有存活的對象都向一端移動,然后清理掉端邊界以外的內(nèi)存.
4. 方法區(qū)回收(永久代)
在方法區(qū)進行垃圾回收一般”性價比”較低, 因為在方法區(qū)主要回收兩部分內(nèi)容: 廢棄常量和無用的類.
回收廢棄常量與回收其他年代中的對象類似, 但要判斷一個類是否無用則條件相當(dāng)苛刻:
- 該類所有的實例都已經(jīng)被回收, Java堆中不存在該類的任何實例;
- 該類對應(yīng)的Class對象沒有在任何地方被引用(也就是在任何地方都無法通過反射訪問該類的方法);
- 加載該類的ClassLoader已經(jīng)被回收. 但即使?jié)M足以上條件也未必一定會回收, Hotspot VM還提供了-Xnoclassgc參數(shù)控制(關(guān)閉CLASS的垃圾回收功能). 因此在大量使用動態(tài)代理茎芋、CGLib等字節(jié)碼框架的應(yīng)用中一定要關(guān)閉該選項, 開啟VM的類卸載功能, 以保證方法區(qū)不會溢出.
分區(qū)收集
分區(qū)算法則將整個堆空間劃分為連續(xù)的不同小區(qū)間, 每個小區(qū)間獨立使用, 獨立回收. 這樣做的好處是可以控制一次回收多少個小區(qū)間
在相同條件下, 堆空間越大, 一次GC耗時就越長, 從而產(chǎn)生的停頓也越長. 為了更好地控制GC產(chǎn)生的停頓時間, 將一塊大的內(nèi)存區(qū)域分割為多個小塊, 根據(jù)目標(biāo)停頓時間, 每次合理地回收若干個小區(qū)間(而不是整個堆), 從而減少一次GC所產(chǎn)生的停頓
三、垃圾回收器
Serial:Serial收集器是Hotspot運行在Client模式下的默認(rèn)新生代收集器, 它在進行垃圾收集時蜈出,會暫停所有的工作進程田弥,用一個線程去完成GC工作
特點:簡單高效,適合jvm管理內(nèi)存不大的情況(十兆到百兆)铡原。
Parnew:ParNew收集器其實是Serial的多線程版本偷厦,回收策略完全一樣,但是他們又有著不同燕刻。
我們說了Parnew是多線程gc收集只泼,所以它配合多核心的cpu效果更好,如果是一個cpu卵洗,他倆效果就差不多请唱。(可用-XX:ParallelGCThreads參數(shù)控制GC線程數(shù))
Cms:CMS(Concurrent Mark Sweep)收集器是一款具有劃時代意義的收集器, 一款真正意義上的并發(fā)收集器, 雖然現(xiàn)在已經(jīng)有了理論意義上表現(xiàn)更好的G1收集器, 但現(xiàn)在主流互聯(lián)網(wǎng)企業(yè)線上選用的仍是CMS(如Taobao),又稱多并發(fā)低暫停的收集器弥咪。
由他的英文組成可以看出,它是基于標(biāo)記-清除算法實現(xiàn)的十绑。整個過程分4個步驟:
- 初始標(biāo)記(CMS initial mark):僅只標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象, 速度很快
- 并發(fā)標(biāo)記(CMS concurrent mark: GC Roots Tracing過程)
- 重新標(biāo)記(CMS remark):修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄
- 并發(fā)清除(CMS concurrent sweep: 已死對象將會就地釋放)
可以看到聚至,初始標(biāo)記、重新標(biāo)記需要STW(stop the world 即:掛起用戶線程)操作本橙。因為最耗時的操作是并發(fā)標(biāo)記和并發(fā)清除扳躬。所以總體上我們認(rèn)為CMS的GC與用戶線程是并發(fā)運行的。
優(yōu)點:并發(fā)收集甚亭、低停頓
缺點:
- CMS默認(rèn)啟動的回收線程數(shù)=(CPU數(shù)目+3)*4 當(dāng)CPU數(shù)>4時, GC線程最多占用不超過25%的CPU資源, 但是當(dāng)CPU數(shù)<=4時, GC線程可能就會過多的占用用戶CPU資源, 從而導(dǎo)致應(yīng)用程序變慢, 總吞吐量降低.
- 無法清除浮動垃圾(GC運行到并發(fā)清除階段時用戶線程產(chǎn)生的垃圾)贷币,因為用戶線程是需要內(nèi)存的,如果浮動垃圾施放不及時亏狰,很可能就造成內(nèi)存溢出片择,所以CMS不能像別的垃圾收集器那樣等老年代幾乎滿了才觸發(fā),CMS提供了參數(shù)-XX:CMSInitiatingOccupancyFraction來設(shè)置GC觸發(fā)百分比(1.6后默認(rèn)92%),當(dāng)然我們還得設(shè)置啟用該策略-XX:+UseCMSInitiatingOccupancyOnly
- 因為CMS采用標(biāo)記-清除算法骚揍,所以可能會帶來很多的碎片字管,如果碎片太多沒有清理,jvm會因為無法分配大對象內(nèi)存而觸發(fā)GC信不,因此CMS提供了-XX:+UseCMSCompactAtFullCollection參數(shù)嘲叔,它會在GC執(zhí)行完后接著進行碎片整理,但是又會有個問題抽活,碎片整理不能并發(fā)硫戈,所以必須單線程去處理,所以如果每次GC完都整理用戶線程stop的時間累積會很長下硕,所以XX:CMSFullGCsBeforeCompaction參數(shù)設(shè)置隔幾次GC進行一次碎片整理(默認(rèn)為0)丁逝。
G1:同優(yōu)秀的CMS垃圾回收器一樣,G1也是關(guān)注最小時延的垃圾回收器梭姓,也同樣適合大尺寸堆內(nèi)存的垃圾收集霜幼,官方也推薦使用G1來代替選擇CMS。G1最大的特點是引入分區(qū)的思路誉尖,弱化分代的概念罪既,合理利用垃圾收集各個周期的資源,解決了其他收集器甚至CMS的眾多缺陷铡恕。
因為每個區(qū)都有E琢感、S、O代探熔,所以在G1中驹针,不需要對整個Eden等代進行回收,而是尋找可回收對象比較多的區(qū)诀艰,然后進行回收(雖然也需要STW操作柬甥,但是花費的時間是很少的)墙牌,保證高效率。
新生代收集
G1的新生代收集跟ParNew類似暗甥,如果存活時間超過某個閾值喜滨,就會被轉(zhuǎn)移到S/O區(qū)。
年輕代內(nèi)存由一組不連續(xù)的heap區(qū)組成, 這種方法使得可以動態(tài)調(diào)整各代區(qū)域的大小
老年代收集
分為以下幾個階段:
- 初始標(biāo)記 (Initial Mark: Stop the World Event) 在G1中, 該操作附著一次年輕代GC, 以標(biāo)記Survivor中有可能引用到老年代對象的Regions.
- 掃描根區(qū)域 (Root Region Scanning: 與應(yīng)用程序并發(fā)執(zhí)行) 掃描Survivor中能夠引用到老年代的references. 但必須在Minor GC觸發(fā)前執(zhí)行完
- 并發(fā)標(biāo)記 (Concurrent Marking : 與應(yīng)用程序并發(fā)執(zhí)行) 在整個堆中查找存活對象, 但該階段可能會被Minor GC中斷
- 重新標(biāo)記 (Remark : Stop the World Event) 完成堆內(nèi)存中存活對象的標(biāo)記. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并計算所有區(qū)域的活躍度).
- 清理 (Cleanup : Stop the World Event and Concurrent) 在含有存活對象和完全空閑的區(qū)域上進行統(tǒng)計(STW)撤防、擦除Remembered Sets(使用Remembered Set來避免掃描全堆虽风,每個區(qū)都有對應(yīng)一個Set用來記錄引用信息、讀寫操作記錄)(STW)寄月、重置空regions并將他們返還給空閑列表(free list)(Concurrent)