一起聊聊JVM中的垃圾收集器
堆上的對象被判斷未無用對象之后,JVM就會執(zhí)行垃圾清理來釋放內(nèi)存給后續(xù)的創(chuàng)建新對象使用。
圖片
垃圾收集算法(標記-清除算法、標記-復(fù)制算法、標記-整理算法)是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn),下圖是市面上常見的垃圾收集器:
圖片
新生代收集器:Serial、ParNew、Parallel Scavenge;老年代收集器:SerialOld、CMS、Parallel Old;同時支持新生代和老年代收集器:G1、ZGC、Shenandoah。
JDK9之前是新生代和老年代搭配使用;JDK1.9開始默認是G1垃圾收集器。在G1之前是分成新生代和老年代,新生代和老年代使用不同的垃圾收集器,因為新生代和老年代回垃圾的場景不同,所以使用不同的垃圾收集器。
1、新生代垃圾收集器
1.1 serial垃圾收集器
Serial收集器是歷史最悠久也是最基礎(chǔ)垃圾收集器,它是一個單線程工作的收集器,其在進行垃圾收集時必須暫停其他所有工作線程(用戶線程)直到垃圾收集結(jié)束,這也就是STW(stop the world)。
STW會造成在用戶正常使用系統(tǒng)時候把用戶的正常工作的線程全部停掉,進而造成卡頓的現(xiàn)象給用戶的體驗是相當不好的。如下是Serial收集器工作的原理圖:
圖片
垃圾回收不是想什么時候做就可以立即觸發(fā)的,而是需要等待所有的線程運行到安全點后才可以觸發(fā)。
安全點是指代碼中一些特定的位置,當線程運行到這些位置時它的狀態(tài)才是確定的,這樣JVM就可以安全的進行一些操作(如GC)。安全點位置主要有:
(1)方法返回之前
(2)調(diào)用某方法之后
(3)拋出異常的位置
(4)循環(huán)的末尾
當垃圾收集需要中斷線程的時候不是直接對線程操作,僅僅是簡單的設(shè)置一個標志位,各個線程執(zhí)行過程時會不停的主動去輪詢這個標志,一旦發(fā)現(xiàn)中斷標識為true時就會自己在最近的安全點上主動中斷掛起。輪詢標志的地方和安全點是重合的。
假設(shè)一個線程處于sleep或中斷狀態(tài),那么此時它就不能響應(yīng)JVM的中斷請求來運行到安全點,因此JVM引入了safe Region(在一段代碼片段中引用關(guān)系不會發(fā)生變化,這個區(qū)域內(nèi)的任意地方開始GC都是安全的)
serial垃圾收集器采用的是復(fù)制算法,其特點是簡單而高效,但是如果內(nèi)存很大的時候,單線垃圾收集就會使得STW時間變得很長。
1.2 ParNew垃圾收集器
ParNew收集器實質(zhì)上是Serial收集器的多線程并行版本,除了同時使用多條線程進行垃圾收集之外,ParNew和Serial收集器可用的所有控制參數(shù)(如:-XXSurvivorRatio)、收集算法、對象分配規(guī)則、回收策略等都完全一致,如下是 ParNew收集器得工作原理圖:
圖片
ParNew收集器是運行在Server模式下的虛擬機的首選垃圾收集器,ParNew除了Serial收集器搭配使用外,它能與CMS收集器配合工作。
1.3 Parallel Scavenge收集器
Parallel Scavenge收集器是基于標記-復(fù)制算法實現(xiàn)的收集器,也是能夠并行收集的多線程收集器,如下是其工作得原理圖:
圖片
Parallel Scavenge收集器的目標是達到一個可控制的吞吐量(Throughput)。吞吐量是處理器用于運行用戶代碼的時間與處理器總消耗時間的比值,如下是其計算得方式:
吞吐量= 運行用戶代碼時間 / (運行用戶代碼時間+運行垃圾收集時間)
如虛擬機需要完成任務(wù)A,用戶代碼加上垃圾收集總共耗費了10分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是90%。
另外Parallel Scavenge收集器提供了兩個參數(shù)用于精確控制吞吐量,如下所示:
參數(shù) | 含義 |
-XX:MaxGCPauseMillis | 控制最大垃圾收集停頓時間,允許的值是一個大于0的毫秒數(shù),收集器將盡力保證內(nèi)存回收花費的時間不超過用戶設(shè)定值 |
-XX:GCTimeRatio | 設(shè)置吞吐量大小,值應(yīng)當是一個大于0和小于100的整數(shù),也就是垃圾收集時間占總時間的比率 |
縮短垃圾收集停頓時間是以犧牲吞吐量和新生代空間為代價換取的,系統(tǒng)把新生代調(diào)得小一些,收集100MB新生代肯定比收集1G空間快,但是這樣回導(dǎo)致垃圾收集發(fā)生得更頻繁,如原來60秒收集一次、每次停頓100毫秒,現(xiàn)在變成20秒收集一次、每次停頓 50毫秒,這樣停頓時間的確在下降,但吞吐量也降下來了。
直接設(shè)置吞吐量大小,相當于吞吐量的倒數(shù)。如設(shè)置GCTimeRatio為 99,那么就允許最大1%的垃圾收集時間。
Parallel Scavenge收集器與吞吐量關(guān)系密切,所以它也被稱作“吞吐量優(yōu)先收集器”。
2、老年代垃圾收集器
2.2 serial old垃圾收集器
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法。如下是 Serial Old收集器工作原理:
圖片
Serial Old收集器供客戶端模式下的HotSpot虛擬機使用;在服務(wù)端模式下,主要的用途如下:
(1)在JDK5以及之前的版本中與Parallel Scavenge 收集器搭配使用,(2)作為CMS收集器發(fā)生失敗時的后備預(yù)案,即就是在并發(fā)收集發(fā)生Concurrent Mode Failure時使用。
2.2 Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,它是基于標記-整理算法實現(xiàn)的多線程并發(fā)收集,如下是其工作的原理圖:
圖片
在注重吞吐量或CPU資源的場景下,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old 收集器組合來進行垃圾收集。由于Parallel Old底層采用的標記-整理算法,此時STW時間更長一些,如下是標記-整理算法的工作示意圖:
圖片
垃圾標記出來后,不但要清除垃圾而且還要將存活的對象移動整理到內(nèi)存的一側(cè)。
2.3 CMS
CMS(Concurrent Mark Sweep)基于標記-清除算法實現(xiàn)的,它以減少停頓時間(STW)為目標的垃圾收集器,主要用于處理老年代的垃圾回收任務(wù),如下是CMS的工作原理圖:
圖片
CMS垃圾收集器執(zhí)行工作過程相對于其他垃圾收集器來說要更復(fù)雜一些,整個過程分為如下的幾個過程:
(1)初始標記
標記GC Roots能直接關(guān)聯(lián)到的對象,雖然速度很快,但是這個過程會發(fā)生STW。
(2)并發(fā)標記
從GC Roots 的直接關(guān)聯(lián)對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發(fā)運行。
(3)重新標記
為了修正并發(fā)標記期間,因用戶序繼續(xù)運作而導(dǎo)致標記產(chǎn)生變動的那一部分對象的標記記錄,這個階段也會暫停應(yīng)用線程,但耗時較短。
(4)并發(fā)清除
CMS開始并發(fā)的清除已經(jīng)被判斷為已經(jīng)死亡的對象,釋放內(nèi)存空間。這個階段同樣是與應(yīng)用程序并行執(zhí)行的,不會停止應(yīng)用線程。
CMS垃圾收集器具有并發(fā)收集、低停頓的特點,但是CMS垃圾收集器對CPU資源敏感、同時CMS使用的是標記-清除算法清理垃圾,容易產(chǎn)生內(nèi)存碎片。
CMS在并發(fā)階段并沒有STW,所以用戶線程可能會產(chǎn)生更多垃圾,此時就可能出現(xiàn)“Concurrent Mode Failure”失敗進而導(dǎo)致一次STW的Full GC,然后使用serial old垃圾回收器收集垃圾。
總結(jié):
(1)新生代的垃圾回收器有serial、ParNew、Parallel Scavenge;老年大垃圾收集器有Serial Old、CMS、Parallel Old;新生代和老年代收集器:G1、ZGC、Shenandoah。
(2)JDK8中默認的垃圾收集器組合是Parallel Scavenge+Parallel Old;JDK9默認是G1垃圾收集器。
(3)CMS是基于標記-清除算法實現(xiàn)的,Parallel Old基于標記-整理算法實現(xiàn)的。
(4)JDK8中ParNew+CMS組合適用于需要低停頓時間的應(yīng)用,典型的應(yīng)用如電商網(wǎng)站、在線游戲、高并發(fā)服務(wù)器。
(5)JDK8中Parallel Scavenge+Parallel Old適用于多核處理器的高吞吐量應(yīng)用,如科學(xué)計算、數(shù)據(jù)分析、大規(guī)模數(shù)據(jù)處理等場景。