上一篇JVM垃圾收集器與內(nèi)存分配策略(一),下面是jdk1.7版本的垃圾收集器之間的關(guān)系,其中連線兩端的兩種垃圾收集器可以進(jìn)行搭配使用,下面來總結(jié)一下這些收集器的一些特點以及關(guān)系。

一、Serial收集器
1、serial收集器是一個單線程的收集器,單線程說明兩點:①只會使用一個CPU或者一條線程來完成垃圾收集的工作;②在進(jìn)程垃圾收集的時候,必須暫停掉其他所有的工作線程(Stop The World),直到收集結(jié)束。這項收集的工作是虛擬機在用戶不可見的情況下將其正常工作的線程停掉,然后在后臺進(jìn)行自動發(fā)起收集和完成收集。這對于用戶體驗而言是不佳的,但是考慮到防止在收集的時候用戶程序又在產(chǎn)生垃圾,這樣的話效果不好。

2、Serial收集器是虛擬機運行在Client模式下的默認(rèn)新生代收集器,它簡單高效,對于限定在單個CPU的環(huán)境中因為不存在線程切換交互的開銷,單對于垃圾收集而言能夠活著很高的單線程效率。在用戶桌面應(yīng)用程序中,分配給虛擬機管理的內(nèi)存一般不會很大,對于使用少量內(nèi)存的新生代而言,停頓時間一般控制在幾十毫秒活著一百多毫秒,發(fā)生的也不頻繁所以是可以接受的。
二、ParNew收集器
1、上面說到Serial收集器是單線程的版本,而ParNew可以說就是Serial收集器的多線程版本,下面是ParNew收集器的簡單工作流程

2、ParNew收集器是運行在Server模式下面的虛擬機首選的新生代收集器,從最開始虛擬機之間搭配使用關(guān)系的時候可以看出,只有ParNew能夠和CMS(后面會說到Concurrent Mark Sweep)搭配使用(對于Server模式)
3、我們也說到過,ParNew收集器對于在單CPU環(huán)境下面,由于存在線程交互的開銷,使用效果不會比Serial收集器好,當(dāng)然隨著CPU數(shù)量的增加,對于GC時候系統(tǒng)資源的利用率提高和自身收集效率都是有很大好處。
三、Parallel Scavenge收集器
1、Parallel Scavenge收集器也是一個新生代收集器,使用復(fù)制算法,采用并行多線程方式的收集器。
2、Parallel Scavenge收集器的特點在于:它想要達(dá)到一個可控制的吞吐量(運行用戶代碼時間/(運行用戶代碼時間+垃圾收集器運行時間))。對于停頓時間而言,其越短就越適合與用戶交互的程序,良好的響應(yīng)速度能夠提升用戶體驗,較高的吞吐量就可以高效率的利用CPU時間,完成程序執(zhí)行的任務(wù)。
3、Parallel Scavenge收集器提供下面兩個參數(shù)用于控制吞吐量
①-XX:MaxGCPauseMillis?用于設(shè)置最大垃圾收集停頓時間的參數(shù):MaxGCPauseMillis的數(shù)值是一個大于0的毫秒數(shù),垃圾收集器將盡可能保證在設(shè)置的時間內(nèi)完成工作。這里要注意的是:GC停頓時間是以犧牲吞吐量和新生代空間來換取的,如果將新生代調(diào)小一些,那么垃圾收集也會變得更加頻繁。比如說將新生代空間由500M調(diào)整為300M,將收集頻率由10秒變?yōu)?s,相對而言收集變得更加頻繁,然后停頓時間由100ms變?yōu)?0ms,這樣的話雖然停頓時間縮短了,但是系統(tǒng)的吞吐量也變小了。
②-XX:GCTimeRatio?直接設(shè)置吞吐量大小的參數(shù):其值是一個>0且<100的整數(shù),也就是垃圾收集時間占總時間的比例。比如將參數(shù)設(shè)置為19,那么允許的最大GC時間就是總時間的5%=1/(1+19),默認(rèn)值是99%=1/(1+99)
4、Parallel Scavenge收集器也被稱為吞吐量優(yōu)先的收集器,除了上面的參數(shù)之外,它還提供一種自適應(yīng)策略,
+XX:UserAdapterSizePolicy:這是一個開關(guān)參數(shù),當(dāng)這個參數(shù)打開之后,不需要手動指定新生代的大小(-Xmn)、Eden區(qū)和Survivor區(qū)的比例、晉升老年代對象年齡大小等參數(shù)了,虛擬機將會根據(jù)當(dāng)前系統(tǒng)的運行情況收集性能控制信息動態(tài)調(diào)整這些參數(shù)。
四、Serial Old收集器
1、Serial Old收集器是Serial的老年代版本,同樣是一個單線程的收集器,采用標(biāo)記-整理的算法進(jìn)行收集,主要也是在Client模式下面使用
2、下面是Serial Old的簡單工作流程

五、Parallel Old收集器
1、Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和標(biāo)記-整理算法來進(jìn)行實現(xiàn)
2、下面是Parallel Old收集器的工作流程

3、在最開始介紹各種收集器之間搭配使用的時候,我們可以看到,在Parallel Old收集器出現(xiàn)之前,Parallel Scavenge收集器就只能和單線程的Serial Old收集器搭配使用,而Serial Old在Server應(yīng)用性能上面比較低(多CPU情況下無法充分利用多CPU的處理能力),所以原本注重吞吐量的Parallel Scavenge收集器就不能充分發(fā)揮作用。而使用Parallel Old+Parallel Scavenge收集器則可以充分發(fā)揮多CPU的處理能力,在吞吐量的提高上面比較客觀。
六、CMS收集器
1、CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓回收時間為目標(biāo)的收集器。對于服務(wù)器響應(yīng)速度快、系統(tǒng)停頓時間短的要求能夠很好的滿足。
2、CMS收集器是基于標(biāo)記-清除的算法來進(jìn)行實現(xiàn)的,其工作過程分為下面四個步驟
?、俪跏紭?biāo)記:需要Stop The World,該階段僅僅是標(biāo)記一些GC Roots能夠直接關(guān)聯(lián)到的對象,
?、诓l(fā)標(biāo)記:并發(fā)標(biāo)記階段就是GC Tracing的過程,標(biāo)記與GC Roots間接關(guān)聯(lián)的對象,不需要Stop The World
?、壑匦聵?biāo)記:修正并發(fā)標(biāo)記階段因用戶程序繼續(xù)執(zhí)行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象,這一階段的時間開銷會比初始標(biāo)記稍長,但是會比并發(fā)標(biāo)記短
?、懿l(fā)清除:進(jìn)行垃圾收集
3、下面是CMS收集器的工作流程:其中耗時比較長的并發(fā)標(biāo)記和并發(fā)清除都可以和用戶程序一起執(zhí)行,采用并發(fā)的方式減少了時間停頓

4、下面是CMS收集器的缺點
?、儆捎谒牟l(fā)性,導(dǎo)致其對于CPU的資源比較敏感,如果硬件資源不夠,那么CMS收集器的效率會很低;除此之外,雖然CMS工作時候不會導(dǎo)致用戶程序停頓,但是會占用CPU的一部分資源導(dǎo)致用戶程序忽然變慢,總的吞吐量會降低。
?、贑MS收集器無法處理浮動垃圾,可能會出現(xiàn)并發(fā)標(biāo)記失敗而提前出發(fā)一次Full GC。由于CMS并發(fā)清理階段用戶程序還在繼續(xù)運行,就會產(chǎn)生新的垃圾對象,這一部分出現(xiàn)在標(biāo)記過程之后,CMS無法在當(dāng)次收集的過程中處理掉,就只能留待下一次進(jìn)行處理,這些就是浮動垃圾。
?、跜MS收集結(jié)束會產(chǎn)生大量空間碎片。CMS是基于標(biāo)記-清除算法來進(jìn)行實現(xiàn)的收集器,這種收集算法就會導(dǎo)致比較多的空間碎片,而且在無法找到足夠的連續(xù)空間來分配當(dāng)前對象的時候,就會提前觸發(fā)一次Full GC。CMS收集器提供-XX:+UseCMSCompactAtFullCollection參數(shù)用于不得不觸發(fā)Full GC的時候進(jìn)行內(nèi)存碎片的整理合并(但是這種方式的缺點也很顯然:內(nèi)存整理的過程是無法并發(fā)的,就會導(dǎo)致停頓時間變長)。
七、G1收集器
1、G1收集器的特點:
①并行與并發(fā):CPU利用多CPU、多個核的特點,使用多個CPU來縮短Stop The? World的時間
②分代收集:G1采用不同與其他方式去處理新創(chuàng)建的對象和已經(jīng)存活一段時間(熬過多次GC)的對象
?、劭臻g整合:G1整體上采用的是標(biāo)記-整理算法來實現(xiàn),局部上看則是復(fù)制算法實現(xiàn),而這兩種算法都不會產(chǎn)生空間碎片
④可預(yù)測的停頓:G1除了降低停頓之外,還建立可預(yù)測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),控制消耗在垃圾收集上面的時間不超過N秒
2、在G1收集器的眼里,它將Java堆分為多個大小相等的區(qū)域(Region),雖然還保留有新生代和老年代的概念,但是都是一個Region的集合,不再是物理隔離的了。
3、G1能夠建立可預(yù)測的停頓,是因為能有計劃的避免在Java堆中進(jìn)行全區(qū)域的垃圾收集。G1追蹤各個Region里面的垃圾堆積回收后所獲得空間大小以及回收所需要的時間值?(作為一個回收價值參考,然后在后臺建立一個優(yōu)先列表,然后在收集的時候根據(jù)允許的收集時間優(yōu)先回收價值最大的Region。
4、關(guān)于G1的化整為零思想
實際上,Region的實現(xiàn)復(fù)雜:考慮將Java堆分為多個Region后,垃圾收集不一定就是按照想要的按照Region為單位的方式進(jìn)行執(zhí)行,因為Region不是孤立的。當(dāng)一個對象分配在某一個Region中,該對象不是只能夠被本Region中的對象進(jìn)行引用,而是可以和整個Java堆中任意的對象發(fā)生引用關(guān)系(換句話說,不同的Region之間其中的對象總是可能存在引用關(guān)系)。這樣的問題不止存在于G1收集器中,在其他的收集器中(新生代中的對象可能和老年代的對象有引用關(guān)系)同樣存在,那么GC的時候效率就會降低很多(掃描整個Java堆)。
在G1和其他分代收集器中,處理不同區(qū)域之間的對象引用的方式是這樣的:使用Remember Set來避免全堆掃描。G1中每個Region都有一個與之對應(yīng)的Remember Set,虛擬機在發(fā)現(xiàn)對Reference 類型的數(shù)據(jù)進(jìn)行Write操作的時候,會首先觸發(fā)一個Write Barrier中斷操作,去檢查該Reference對象是否在其他Region存在引用(在其他分代收集器中就是檢查老年代中的對象是否引用了新生代中的對象),如果存在的話就會將相關(guān)引用信息記錄到被引用對象所屬的Region的Remember Set中。這樣在進(jìn)行內(nèi)存回收的時候,在GCRoot的枚舉范圍之內(nèi)加入Remember Set就能保證不對整個Java堆掃描也不會遺漏存在引用關(guān)系的對象。
5、下面是G1收集器的簡單工作流程

?、俪跏紭?biāo)記:僅僅標(biāo)記一些能與GC Roots之間關(guān)聯(lián)到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發(fā)執(zhí)行的時候,能在正確可用的Region中創(chuàng)建對象(需要Stop The World)
②并發(fā)標(biāo)記:從GC Roots開始進(jìn)行可達(dá)性分析,找出存活的對象,這一階段用戶線程的執(zhí)行可能會改變引用關(guān)系,JVM會將標(biāo)記變動記錄線程的Remember Set Logs里面,在最終標(biāo)記階段合并到Remember Set中
?、圩罱K標(biāo)記:修正在并發(fā)標(biāo)記階段因為用戶程序繼續(xù)執(zhí)行導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分標(biāo)記記錄,并發(fā)標(biāo)記階段中Remember Set Logs中的數(shù)據(jù)合并到Remember Set中,需要Stop The? World,可以并行執(zhí)行
?、芎Y選回收:首先對各個Region的回收價值和成本進(jìn)行排序,根據(jù)所期望的GC停頓時間來指定回收計劃(可以和用戶程序一起并發(fā)執(zhí)行)
6、優(yōu)點
?、倥cCMS收集器相比:不會產(chǎn)生空間碎片(按照Region為單位進(jìn)行回收,并且采用的是標(biāo)記整理算法來實現(xiàn));并發(fā)階段可以為工作線程預(yù)留足夠的空間(單獨分配空閑的Region空間提供使用);有可以預(yù)測的停頓時間
八、內(nèi)存分配與回收策略
1、對象優(yōu)先在Eden區(qū)上分配
a)關(guān)于Minor GC和Full GC
?、費inor GC:新生代GC,指發(fā)生在新生代的垃圾收集動作,因為Java新生對象大多數(shù)具有朝生夕滅的特點,所以新生代GC發(fā)生的比較頻繁,回收速度也比較快
?、贔ull GC:老年代GC,也叫MajorGC;發(fā)生在老年代,一般回收速度比Minor GC慢10倍。
b)一般情況下,對象優(yōu)先在Eden區(qū)分配,當(dāng)Eden區(qū)沒有足夠的空間的時候,將會觸發(fā)一次Minor GC;
c)下面使用一個簡單的例子,然后通過-XX:+PrintGCDetails查看GC日志信息
1package cn.jvm.test; 2 3publicclass Test08 { 4privatestaticfinalinttest = 1024 * 1024; 5//-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 6publicstaticvoid main(String[] args) { 7byte[] t1, t2, t3, t4; 8t1 =newbyte[2 * test]; 9t2 =newbyte[2 * test];10t3 =newbyte[2 * test];//這個時候Eden區(qū)已經(jīng)分配了6M內(nèi)存,而Survivor區(qū)只有1M大小11t4 =newbyte[4 * test];12? ? }13}
?、偕厦娴睦又校覀兪褂?Xms20M設(shè)置堆空間初始大小為20M, -Xmx20M設(shè)置堆空間最大為20M, -Xmn10M設(shè)置新生代大小為10M, -XX:+PrintGCDetails 打印GC日志信息,-XX:SurvivorRatio=8設(shè)置新生代中Eden:From:To = 8:1:1
?、谙旅媸荊C日志信息

2、大對象直接進(jìn)入老年代
a)大對象就是指那些需要連續(xù)內(nèi)存空間的Java對象,典型的就是很長的字符串或者數(shù)組(盡量避免使用朝生夕滅的大對象,因為會使虛擬機不得不提前出發(fā)GC找到足夠的連續(xù)空間進(jìn)行分配)
b)虛擬機提供-XX:PretenureSizeThreshold參數(shù),用于設(shè)置大于這個值的時候?qū)ο笾苯釉诶夏甏M(jìn)行分配(避免在Eden區(qū)和Survivor區(qū)發(fā)生大量的內(nèi)存復(fù)制)
c)下面是一個簡單的例子,和打印的GC日志信息
1package cn.jvm.test;23publicclass Test09 {4publicstaticvoid main(String[] args) {5byte[] test =newbyte[6 * 1024 * 1024];6? ? }7}
?、偕厦娴睦又?,我們使用-XX:PretenureSizeThreshold=31400000設(shè)置大于這個值的時候,分配對象直接分配在老年代
?、谙旅媸荊C日志信息

3、長期存活的對象直接進(jìn)入老年代
a)虛擬為每個分配的對象定義了一個對象年齡計數(shù)器,如果新生對象在Eden區(qū)并經(jīng)過一次GC后被成功復(fù)制到Survivor區(qū)之后,就會將其對象年齡設(shè)置為1,之后每次熬過一次GC都會將對象年齡加1,當(dāng)年齡達(dá)到一定的大?。J(rèn)15)的時候,就會進(jìn)入老年代。
b)虛擬機提供-XX:MaxTenuringThreshold參數(shù)設(shè)置年齡的閾值
4、動態(tài)對象年齡判定
除了按照對象的年齡來判斷對象是否需要進(jìn)入老年代,在實際當(dāng)中,并不是硬性要求對象的年齡必須達(dá)到XX:MaxTenuringThreshold設(shè)置的值才會進(jìn)入老年代,通常情況下:當(dāng)Survivor空間中所有同齡對象的大小總和超過了空間的一半,那么年齡大于等于該年齡的對象就直接分配在老年代
5、空間分配擔(dān)保機制
a)首先,我們需要知道新生代都采用復(fù)制算法,當(dāng)Survivor空間不夠存放存活的對象的時候,就需要老年代進(jìn)行分配擔(dān)保。如果老年代的剩余空間大于新生代所有對象之和,那么擔(dān)保是沒有問題的,但是如果新生代的所有存活對象都大于Survivor,那么這些對象就會因為進(jìn)入老年代導(dǎo)致老年代空間不足而觸發(fā)FullGC,此時的老年代擔(dān)保是有風(fēng)險的。
b)對于這個風(fēng)險的問題,JVM會查看參數(shù)HandlePromotionFailure參數(shù)(是否允許冒險)的設(shè)置,如果不允許就會直接進(jìn)行FullGC,如果允許的話就會先查看一下每次進(jìn)入老年代的對象大小之和的平均值(如果老年代的剩余空間大小大于這個平均值,就認(rèn)為可以冒險),但是如果在冒險進(jìn)行Minor GC的時候發(fā)現(xiàn)新生代100%對象都存活(這是一種極端情況),那么還是會進(jìn)行FullGC??雌饋頁?dān)保失敗的時候繞了比較大的彎子,但是為了避免FullGC的頻繁出現(xiàn)還是會將這種冒險的行為設(shè)置為true(JDK6Update24之后的默認(rèn)設(shè)置)