上圖基本列出了HotSpot虛擬機中的幾種垃圾收集器。其中藍線部分表示這些垃圾收集器可以組合搭配使用。
由于J2SE 5.0開始,jvm會默認根據其運行的操作系統等因素,自動選擇比較合適的垃圾收集器。一般來說這種選擇還是不錯的,當然也可以通過參數指定你想要使用的收集器。
如果不清楚當前用的是哪種收集器,下面的代碼可以幫助進行識別
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;
public class GCInformation {
public static void main(String[] args) {
List<GarbageCollectorMXBean> gcMxBeans =
ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gcMxBean : gcMxBeans) {
System.out.println(gcMxBean.getName());
}
}
}
下面大致挑選Parallel Old、CMS、G1收集器簡單說明下
Parallel Old
Parallel Old很適合于批處理、billing、payroll、科學計算等類型的任務。一般這些任務要求的堆內存比較大,要求整體的吞吐量高,并且可以接受出現較長時間的垃圾收集引起的停頓。
老生代的收集消耗受到其存儲對象數量多少的影響,而不是堆本身的大小。因此可以用更多的內存換取更高的吞吐量,并且接受時間更長但次數更少的收集停頓。
CMS - Concurrent Mark Sweep
1. 并發
提到CMS,其首先引人注目的就是其提供的并發性Concurrent,這不同于Serial Old的串行,代表著CMS可以與用戶線程同時工作。不過實際上,這里的Concurrent前需要加一個修飾 - the mostly,也就是說從壞的角度解釋,它仍然不是全并發的。
之所以說CMS是 the mostly的,有幾個原因。首先是其實現上采取了多階段完成一個回收周期中的標記、清除任務的。這里有4個階段,其中有些階段是并發的,有些仍然是需要stop the world的:
- 初始標記 - STW
- 并發標記 - Concurrent
- 重新標記 - STW
- 并發清除 - Concurrent
從上面可以看出,1和3的階段仍然不能用并發方式完成。但是這兩個階段的用時是很短的,因為初始標記僅僅是標記一下gc roots能直接關聯到的對象,而重新標記是檢查并發標記階段用戶線程導致標記發生了變動的那一部分對象的標記記錄,雖然其時間要長于初始標記,但仍然遠遠短于并發標記階段。
而并發標記就是通過gc roots進行tracing的過程,耗時很長。最后,當jvm確定所標記的對象沒有問題了,就會進入并發清除階段,由于這時的對象已經明確是死亡的了,所以可以安全的進行回收。這兩個階段都耗時較長,并且可以與用戶線程并發執行。
為了對標記階段實現并發,CMS需要解決concurrent marking race的問題,這個問題是由于并發執行時,用戶線程可能將一個CMS還未檢查區域中的引用拷貝到已檢查區域中,導致CMS在回收時誤將該引用對應的對象進行了回收,進而破壞了堆。
有兩種方法解決這個問題:incremental update和“snapshot at the beginning” - SATB。CMS采用了后者,它在標記周期開始時,抓取存活對象的快照,并利用pre-write barrier來跟蹤快照中對象的變化。由于HotSpot采用分代收集,并且已經有write barrier來跟蹤對象引用的變化,并記錄在card table中,因此,在標記階段,我們可以用某種方式清理card table,這樣清理之后card table中累計的新變化就是并發標記階段用戶線程引起的新變化,收集器可以重新檢測這些變化并重新進行標記。由于這種用戶引發的變化會不斷發生,因而上面的過程會不斷重復執行。最終,當收集器發現剩下的標記任務量很小的時候,它就會出發一個很短的stop the world過程,完成最后剩下的重新標記過程。
2. 并發失敗
由于標記、清除是并發執行的原因,如果用戶線程引起的對象變化快于處理的過程,導致CMS跟不上這個變化速度,就會產生所謂的浮動垃圾floating garbage,這些垃圾只有留待下一次回收周期中處理。為了保證浮動垃圾的存在不會導致用戶線程沒有內存可用,CMS需要預留一部分空間供并發收集時給用戶線程使用量,例如jdk 1.5中,默認老生代使用到68%就會激活垃圾回收。如果連預留的這部分內存也不夠使用了,就可能引起concurrent mode failure的問題,當其發生時,會記錄到GC的日志。這時就需要Serial Old這個備用收集器接管老生代的收集工作。
3. 碎片與full gc
CMS的老生代清理使用free list記錄空出來的內存空間,它并不會每次都對內存進行整理compaction,因而會產生內存碎片。最終當碎片太多,導致新生代的對象無法升級promotion到老生代中時,就會引發一次full gc來進行compact,這個過程是stop the world的。如果在gc日志中看到“promotion failure”,或者出現大于1、2秒的停頓,那說明發生了提升失敗。
JVM提供了-XX:+UseCMSCompactAtFullCollection來完成上面的compact執行過程,其默認開啟,使得CMS在每次full gc都會進行碎片整理。但這導致了停頓時間變長。因此又提供了-XX:CMSFullGCsBeforeCompaction來設置執行多少次不compact的full gc后再執行一次帶compact的收集。
4. CPU
由于并發的原因,在該階段收集器會與用戶線程共享cpu資源。這相應的會降低應用的總吞吐量。CMS默認開啟的并發線程是 (CPU數 + 3)/ 4,也就是當CPU數量大于4時,回收會利用最高25%的cpu資源,并隨著CPU數量增加這個比例減少。但當CPU數量小于4時,垃圾回收對用戶線程的影響就比較大了。相比Parallel收集器,CMS減少的應用吞吐量大致是10% - 40%。
總的來說,CMS的停頓時間低,適用于桌面UI應用、相應用戶請求的Web Server、響應數據庫查詢的服務等要求低停頓的場景。
G1 - Garbage First
G1收集器在技術實現上有很多和老一輩收集器相似的地方。例如,它同樣使用monolithic、STW的方式收集新生代,使用和CMS很相似的the mostly concurrent標記階段,具有concurrent sweep階段,新生代區同樣由eden和survivor的regions組成,使用STAB解決并發標記競爭的問題,等等。
當然,它也有不同于其他收集器的地方。首先是其內存區域劃分為多個大小相等的region,雖然仍然有分代的概念,但從物理上新生代和老生代不再是隔離的兩個區域,它們分別由一些region組成,并且在物理上不要求這些region在內存中必須相互連續的占據一段空間,如下圖所示。
另一個不同的地方是其處理compaction的方式,G1使用一種被稱為“incremental compaction”的技術,該技術用于盡量避免full gc的發生。由于G1也使用了remembered set來跟蹤region間的對象引用,如果當heap中沒有任何引用指向某一個region,那么該region就可以被安全的compact,并且不用做remap的工作。
該方法也顯露了一個現象,就是有些region比其他region使用得更頻繁,通俗的說就是更受歡迎。G1的一些實現就是基于這個假設的,但這也是G1的問題之一,因為該假設并非總是正確的。
1. G1的Remembered Set
由于G1使用region來分塊管理對象,因此以前的收集器使用remembered set來跟蹤老生代向新生代中對象的引用,就擴展到了每個region之間進行跟蹤。這使得實現變得更復雜,每個region都有其自己相關的一個RS,因此RS的總量可能很大。
當虛擬機發現程序對引用類型的數據進行寫操作時,會產生一個write barrier暫時中斷這個寫操作,并檢查引用對象是否處于不同的region中。如果是,就通過CardTable把相關引用信息記錄到被引用對象所屬region的RS中
2. 收集過程
大致上,G1的收集過程也可以劃分為4個階段:
- 初始標記 - STW
- 并發標記 - Concurrent
- 最終標記 - STW、Parallel
- 篩選回收
可以看出,這里的前3個過程和CMS的前3個過程很相似。當然細節實現還是有差別的。
對于最終標記階段,虛擬機將并發標記這段時間對象的變化記錄在線程的Remembered Set Logs中,并在最終標記階段將Remembered Set Logs的數據合并到Remembered Set中,因此需要停止用戶線程,引發STW,但可以并行的執行(Parallel)。
對于篩選回收階段,虛擬機對各個region的回收價值和成本進行排序,形成一個優先列表,這也是Garbage First名稱由來的原因。然后根據用戶期望的gc停頓時間來制定回收計劃。其實從官方透露的信息看也可以做到Concurrent,但是由于只回收一部分region,使用STW可以大幅提高效率。