JVM隨筆記錄4-垃圾回收器ParNew+CMS
垃圾回收器
今天記錄一下幾種垃圾回收器,分別記錄下幾種垃圾回收器不同的機制
1.ParNew垃圾回收器
ParNew垃圾回收器用於回收新生代的垃圾,使用的垃圾回收演算法用的就是上節中說的標記複製演算法,將新生代eden區中的存活物件標記出來,然後將存活物件複製到一個survive 1區中,然後將eden區中垃圾物件清理掉,下次再滿了之後將存活物件複製到suevive 2區中,然後清理掉垃圾物件。ParNew垃圾回收器優化的地方在於可以多執行緒併發的進行垃圾回收,ParNew垃圾回收器預設開啟的垃圾回收執行緒是跟機器的CPU是一致的,比如機器是4核CPU,那麼此時ParNew的垃圾回收執行緒就是4個執行緒,當然,這個執行緒數量是可以進行手動指定的,可以使用引數-XX:ParallelGCThreads
由此可以看出,使用ParNew垃圾回收器比原始的serial垃圾回收器的區別在於,serial是單執行緒的,而ParNew是多執行緒的,所以一般情況下,ParNew垃圾回收器的效能是更好的。
2.CMS垃圾回收器
CMS垃圾回收器用於回收老年代的垃圾,之前有講過新生代物件有以下幾種情況會進入到老年代中:
1.分代年齡達到15的物件會轉移到老年代中
2.新生代minor GC後存活的物件比survive區大,這些物件會轉移到老年代中
3.survive區中部分物件大小已經超過了survive區記憶體的一般,那麼超過這部分物件分代年齡的物件將轉移到老年代中
4.大物件會直接進入到老年代中
老年代觸發full gc的條件是:
1.老年代可用空間小於歷次進入老年代存活物件的平均大小
2.minor gc後進入老年代的存活物件,老年代空間不夠 放不下
之前有說過老年代使用的垃圾演算法是標記整理,將老年代中存活物件標記出來後,然後將這些存活物件緊湊的排列在一起,在將其他垃圾物件回收掉,這個過程是非常慢的,老年代存活物件多,既要標記這麼多存活物件,還要將這些物件排列在一起,因此,CMS垃圾回收器在此基礎上做出一些改進,來優化老年代的垃圾回收,CMS的垃圾回收演算法分為4個階段:初始標記->併發標記->重新標記->併發清理
1.初始標記
初始標記階段,會停掉系統的所有工作執行緒,進入“STOP THE WORLD”階段,所謂的“初始標記”,就是標記出GC ROOT直接引用的物件,如下,初始標記會根據“b”這個類的靜態變數代表的GC ROOT 去標記出它直接引用的B物件,這個就是初始標記的過程。這裡是不會去管C這種物件的,因為物件C是被B類的“c”例項變數引用的,方法的區域性變數和類的靜態變數是GC ROOT,而類的例項變數則不是,所以初始標記的速度是很快的,雖然會暫停所有的工作執行緒,但是速度很快,其實影響不是很大
public class A{ private static B b = new B(); } public class B{ private C c = new C(); }
2.併發標記
併發標記是垃圾回收執行緒跟系統的工作執行緒併發工作,不會影響程式的正常執行,在執行期間,可以建立物件,也可能部分物件失去引用變成垃圾物件;這一過程中垃圾回收執行緒會對已有的物件進行gc root追蹤,比如這裡的例項物件c,被物件B引用了,而物件B是物件A的靜態變數引用,那麼此時可以認定物件C是被GC ROOT間接引用的,所以C也是不能被回收的,C在這一步中會被標記為存活物件,所有像C這樣根據gc root判斷為間接引用的物件在這一步都會被標記為存活物件,所以併發標記的過程是很緩慢的,但是是跟系統工作執行緒併發執行的,所以是不會對系統執行造成影響的
3.重新標記
在併發標記階段中,一邊標記存活物件和垃圾物件,垃圾回收執行緒工作的時候,系統的其他工作執行緒還是會生成新的物件以及一些物件會成為垃圾物件,所以這第三步重新標記就是把這些新增和變更的物件重新標記一下,而這重新標記是會stop the world,停止系統程式,然後重新標記第二階段產生的物件和一些失去引用變成垃圾的物件,這個階段只是對少量的物件進行標記處理,所以這一步是相當塊的,所以影響也不是很大
4.併發清理
老年代中存活的物件都已經標記出來,在第四步就可以開始清理掉垃圾物件了,這一步的清理是垃圾回收執行緒是併發執行的,不會stop the world,清理階段相對來說是比較耗時,但併發處理不會影響系統執行
綜上,CMS的垃圾回收分為以上4個步驟,在原始的垃圾回收演算法上進行了優化處理,可以看出,處理相對耗時的是第2和第4不,但這兩不是併發處理的,所以對系統沒有影響,而第1、第3步的處理速度是比較塊的,因此CMS的垃圾回收演算法相比較下對系統影響比較小。
CMS在做出優化的同時也帶來了幾個細節問題:
1.併發回收垃圾會導致CPU資源緊張
CMS第2步和第4步時垃圾回收執行緒跟工作執行緒併發執行,這會導致有限的CPU資源被垃圾回收執行緒佔用一部分,CMS預設啟動的垃圾回收執行緒數量是(CPU核數 + 3)/4,比如一個4核8G的機器,垃圾回收執行緒就有(4+3)/4 = 1個,會佔用一個CPU資源。
2.Concurrent Mode Failure問題
在併發清理階段,由於是併發處理的,這個時候也會有可能有新的物件進入到老年代中,同時還變成了垃圾物件,這個垃圾物件稱為“浮動垃圾”,比如在老年代併發清理的時候,新生代進行minor gc,可能一些物件進入到老年代,然後這些物件失去引用,那麼這些物件就成為了 浮動垃圾,雖然他們是屬於垃圾物件,但是由於在併發清理步驟前是沒有被標記的,所以這些物件是不會被回收的,需要等到下一次GC的時候才能對這些物件進行回收處理。所以CMS為了保證在併發清理階段老年代有足夠的記憶體來存放這些物件,一般會在老年代中預留部分空間,也就是說CMS觸發垃圾回收是會在老年代記憶體佔用的空間達到一定的比例時觸發,可以通過引數“-XX:CMSInitiatingOccupancyFaction”來設定老年代記憶體已使用多少比例就觸發CMS垃圾回收,這裡jdk預設的值是92%,也就是說老年代中物件佔用了92%的空間,就會觸發CMS垃圾回收。
那麼,如果在CMS垃圾回收期間,可能發生新生代minor GC後進入老年代的物件大於老年代的可用記憶體的情況,這個時候就會發生Concurrent Mode Failure,CMS一邊回收垃圾,一邊又有新的物件進來,然後老年代空間不夠放新的物件了,這個時候就會使用“Serial Old”垃圾回收器來替代CMS垃圾回收器,就是直接把程式進入 stop the world狀態,停止所有工作執行緒,專心進行垃圾回收處理,重新進行GC ROOT追蹤 標記存活物件和垃圾物件,重新進行垃圾清理。清理掉之後再回復系統。
3.記憶體碎片問題
再併發清理之後老年代會存在有垃圾碎片的問題,也就是CMS採用了“標記-清理”的演算法,每次標記出垃圾物件,然後一次性回收到,會導致大量的垃圾碎片,太多的垃圾碎片就會導致頻繁的full gc,所以CMS不是僅僅使用“標記-清理”演算法,這裡CMS還有兩個引數來控制清理垃圾之後對老年代進行碎片整理,就是把存活物件挪到一塊。第一個是“-XX:+UseCMSCompactAtFullCollection”,這個引數預設是開啟的,它的意思就是CMS full gc後需要再次進入“stop the world”,停止掉工作執行緒,然後進行碎片整理,將存活都物件挪到一塊。避免記憶體碎片。第二個引數是“-XX:CMSFullGCsBeforeCompaction”,這個意思是多少次FULL GC後執行一次碎片整理工作,預設值是0,也就是隻要進行FULL GC就會進行碎片整理。
補充:之前有提到觸發老年代垃圾回收有以下三個情況:
1.如果沒有開啟空間擔保規則引數,老年代可用空間小於新生代所有物件的大小,會進行FULL GC,這個引數一般是開啟的。
2.空間擔保規則,歷次進入老年代物件的平均大小大於老年代可用空間
3.新生代minor gc後存活物件大於survive區大小而進入到老年代,而老年代空間不夠存放這些物件,也會觸發FULL GC。
4.這裡再補充一個老年代觸發的條件,CMS垃圾回收器會判斷當老年代空間已使用引數“-XX:CMSInitiatingOccupancyFaction”設定的比例後,也會進行FULL GC