【5】JVM-垃圾收集器
通過學習了解到現在商用的JVM中的垃圾收集采用的是分代收集算法,即針對不同年代采用不同的收集算法。在JVM中,GC主要作用於堆內存中,堆內存又被劃分為新生代和老年代,由於新生代對象絕大多數是朝生夕死,而老年代相對存活時間就很長,故而需要使用不同的垃圾收集機制,所以垃圾收集器也就分為新生代收集器和老年代收集器,兩者相互組合進行JVM堆內存的空間回收(下圖中相連的垃圾收集器表示可以相互組合,註意Serial Old和CMS也可以聯合進行老年代的垃圾收集)。JDK6u14中開始測試的G1垃圾收集器,正式發布於JDK7u4中,是目前唯一不需要依賴其他垃圾收集器即可完成新生代和老年代內存收集。閱讀之前先了解,GC的兩個指標:暫停時間-應對與存在大量用戶交互的場景;吞吐量-應對後臺計算任務。
- 新生代的垃圾收集器有:Serial收集器、ParNew收集器、Parallel Scavenge收集器
- 老年代的垃圾收集器有:Serial Old收集器、Parallel Old收集器、CMS收集器
- G1收集器。http://f.dataguru.cn/thread-514678-1-1.html
筆者使用的是JDK7u51,也就是JDK1.7.0_51
下面我將試著通過自己的理解來分析各個垃圾收集器的特點,目前並沒有一個適用於任何場景的垃圾收集器,所以選擇何種垃圾收集器進行配合是根據具體應用來區別對待的,那麽了解各種垃圾收集器的特點以及他們之間是否可以相互配合,就十分重要了。
垃圾收集器運行過程中必然會發生“Stop the world”,只是時間長短和暫停時間可不可控的區別。
在進行下面的閱讀之前,首先明確在垃圾收集器中,“並發”和“並行”這兩個概念的差別:
- 並行(Parallel):多個垃圾收集線程並行工作,此時用戶線程處於等待狀態
- 並發(Concurrent):垃圾收集線程和用戶線程同時執行(不一定是並行,可能是交替執行),用戶程序繼續執行,而GC運行在另一個CPU上
- Serial
Serial垃圾收集器,通過這個單詞的意思“連續”,我認為這個應該是指的GC之後內存空間不存在內存碎片的意思,那麽必然不會采用“標記-清除算法”來實現,所以這個垃圾收集器在新生代使用的是“復制算法”,而Serial Old作為Serail收集器的老年代版本,使用的就是“標記-整理算法”。
為什麽先說Serial垃圾收集器,是因為這個收集器是最基本、歷史最悠久的收集器,在JDK1.3.1之前,是JVM新生代收集的唯一選擇。Serial收集器是一個單線程的收集器,這個“單線程”是指JVM在使用它進行GC的時候,必須暫停其他所有的工作線程(sun將這件事情稱為“Stop the world”),直到GC完成,這是一件非常可怕的事情。看到這裏,你可能想我一定要修改我的JVM的新生代收集器,不用Serial了,但是直至現在,Serial依然是JVM在運行Client模式下默認的新生代 收集器。與其他垃圾收集器的單線程相比,Serial簡單而高效。對於用戶桌面應用場景來說,分配給JVM的內存一般不會太大,收集十幾甚至一兩百兆的內存,停頓時間可以控制在幾十毫秒,最多一百多毫秒以內,只要不是特別頻繁,這些停頓還是可以接受的。所以,對於Client模式下的JVM來說,Serial是個很好的新生代收集器,簡單高效。
- ParNew-Parallel New
ParNew收集器也是一個新生代收集器,其實就是Serial收集器的多線程版本,是一個“並行”的垃圾收集器,除了多線程外,其他和Serial差不多。想想也就明白了,當JVM團隊開發出來了Serial,可以滿足Client模式下的JVM,但是對於Server模式下的JVM來說,運行很長時間,有很多的對象需要收集(可能幾十個G),單線程導致的停頓時間太長了(比如每運行1小時需要停頓5分鐘),用戶無法接受業務線程停頓那麽長的時間,我猜測這種情況下那些大牛能想到的最簡單的辦法就是讓Serial變成多線程,這樣開多個線程就可以有效的降低停頓時間,故而這個Serial的多線程版本也就誕生了。
ParNew是許多運行在Server模式下的JVM中首選的垃圾收集器,其中一個重要原因就是除了Serial,它是唯一可以和CMS(Concurrent Mark Sweep)老年代垃圾收集器配合工作。
ParNew在單CPU環境中收集效果不如Serial收集器,但是隨著CPU的增加,它對於GC時系統資源的利用還是很有好處的,默認開啟的線程數與CPU的數量一致 。
- Parallel Scavenge
Parallel Scavenge收集器,簡稱PS收集器,它和ParNew收集器一樣是一個多線程的並行新生代垃圾收集器,一樣采用“復制算法”(始發於JDK1.4.0)。那為什麽還要這個PS收集器呢?現在想一下,ParNew收集器為什麽會產生,不就是閑Serial收集器導致的“Stop the world”的時間太長了嘛,搞個多線程,減少停頓時間。這種的垃圾收集器適合重視服務的響應速度的應用程序(比如購物網站,肯定希望停頓時間越短越好,這樣用戶體驗才好),但是對於一個後臺計算任務(比如MapReduce)來說,沒有太多的交互任務,那麽它所重視的就不是這種響應速度,而是CPU的有效時間利用率(這是我的理解),官方稱之為“吞吐量(Throughtput)”。吞吐量就是指CPU用來運行用戶代碼的時間和CPU的總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+GC消耗的時間)。
Parallel Scavenge收集器正是基於對“吞吐量”的追求而產生的,它的目標就是達到一個可控的吞吐量。由於與吞吐量關系密切,Parallel Scavenge收集器也被稱為“吞吐量優先“收集器。Parallel Scavenge提供了兩個參數用來精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數(單位:毫秒),以及直接設置吞吐量大小的-XX:GCTimeRatio參數(0-100之間,不包括首尾)。GCTimeRatio參數的計算規則是,比如設成19,那麽允許最大時間就占總時間的5%,即1/(1+19),默認值是99,也就是默認允許最大GC時間占比是1%。
Parallel Scavenge收集器還有一個參數來開啟GC的自適應調節策略,只需要將JVM基本內存設置好,並且制定上述兩個參數中的一個來作為JVM的優化目標,那麽JVM就可以根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大吞吐量,這個參數就是-XX:+UseAdaptiveSizePolicy。自適應調節策略也是PS收集器 相對於ParNew收集器的一個重要區別。ParNew收集器需要手工指定新生代大小(-Xmn)、Eden與Survivor的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數。
- Serial Old
- 在JDK1.5和之前的版本中與Parallel Scavenge收集器搭配使用,因為此時CMS還沒有,CMS正式發布於JDK1.6中
- 在JDK1.6和以後的版本中,作為CMS收集器的後備預案,在並發收集發生Concurrent Mode Failure的時候使用。
- CMS
- 初始標記:僅僅標記一下GC Roots能直接關聯到的對象,速度很快
- 並發標記:GC Roots Tracing,梳理引用鏈
- 重新標記:修正並發標記過程中,用戶線程運行導致標記變動的那一部分對象的標記記錄。
- 並發清除
- 對CPU資源非常敏感
- 無法處理浮動垃圾,所謂的浮動垃圾就是CMS並發清除階段用戶線程運行產生的垃圾,這部分垃圾必須等待下一次的垃圾收集來清除。所以CMS執行GC的時候需要預留足夠的內存空間(默認32%,可調節)給用戶線程使用,如果預留空間無法滿足用戶線程的內存需求,那麽就會發生“Concurrent Mode Failure”失敗,然後虛擬機就會啟動Serial Old來重新進行老年代的垃圾收集,這樣就會導致停頓時間很長了。
- 會產生空間碎片(”標記清除“算法的特點),CMS在Full GC發生之後附帶了一次碎片整理過程,而內存整理是無法並發的,導致停頓時間不得不變長。發生這個問題的時候,可能就會調用Serial Old來處理老年代的垃圾回收了。
- Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,簡稱PS Old,使用了多線程和”標記-整理“算法,這個收集器是在JDK1.6中才提供的,在此之前PS new的地位比較尷尬,因為在此之前老年代的垃圾回收只有Serial Old這一種收集器,與Serial Old配合,Parallel Scavenge無法產生理想的回收效果,吞吐量在老年代很大且硬件比較高級的環境中可能還不如使用ParNew與CMS的組合”給力“,而PS Old產生之後,PS New才變得名副其實。
也就是說,在JDK1.6及之後,在註重吞吐量和CPU資源敏感的場合,都可以優先考慮PS New和PS Old的組合。
- G1
在JDK6u14中提供了Early Access版本的G1收集器以供試用,直到JDK7u4的時候才正式發布。G1是一款面向服務端應用的垃圾收集器,HotSpot開發團隊賦予它的使命是未來替換掉CMS收集器,從這點上看,G1也是追求短停頓時間的。
G1收集器與上述的6種收集器相比,具有以下的特點:
- 並行和並發:G1收集器不僅能充分利用多CPU、多核環境下的硬件優勢來減少停頓時間,而且仍可以通過並發的方式讓用戶線程繼續執行
- 分代收集:雖然在使用G1收集器的時候,JAVA堆的內存布局已經不再是物理隔離了,僅僅是邏輯隔離,但是分代的概念得到保留,G1可以獨立管理整個GC堆
- 空間整合:CMS采用了“標記-清除”算法,會產生內存碎片。而G1整體看來采用的是“標記-整理”算法,局部看來采用的是“復制”算法,故而G1運行期間都不會產生內存碎片,這種特性有利於程序長時間的運行。
- 可預測停頓:這是G1相比較CMS的一大優勢,G1除了和CMS一樣追求低停頓外,還能建立可預測的停頓模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不超過N毫秒,這幾乎已經是實時JAVA(RTSJ)垃圾收集器的特征了。
G1最大的特點在我看來就是G1將Java堆劃分成多個大小相等的獨立區域(Region),雖然保留了新生代和老年代的概念,但是它們已經不再是物理隔離了,而都是一部分Region的集合。G1在後臺維護一個 優先列表,這個列表中保存了G1收集到的各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收需要的時間的經驗值),當需要進行垃圾回收的時候,根據用戶允許的收集時間,優先回收列表中價值最大的那個Region(這就是Garbage-First,G1名字的由來)。這種使用Region劃分空間以及根據優先級的區域回收方式,保證了G1收集器可以在有效的時間內獲得盡可能高的收集效率,同時也避免了在整個JAVA堆中進行全區域的垃圾收集。
G1產生的原因我認為就是HotSpot團隊對於低延時和吞吐量兩者同時考慮,不斷追求一個完美的垃圾收集器的產物,雖然在執行流程上和CMS有差不多,並且在初始標記和最終標記階段都需要暫停用戶線程,但是通過重新定義JAVA堆,引出了Region的概念,成功的讓其在性能上能兼顧到低延時和吞吐量,且不需要依賴其他收集器。G1的未來就是優化初始標記和最終標記階段,如果能解決這兩個階段的用戶線程暫停,實現並發,那麽就很有可能產生一個近似理想狀態的一個垃圾收集器。
不過用戶對於新生事物必須的認同需要一定的時間,並且之前垃圾收集器相互配合也可以滿足用戶需求,那麽G1對於大多數公司來說不是必需品,但是,隨著技術的不斷成熟,我認為G1很有可能成為Server模式下的HotSpot默認的收集器。
對於G1收集器,可以參考:http://f.dataguru.cn/thread-514678-1-1.html
【5】JVM-垃圾收集器