1. 程式人生 > >JAVA垃圾回收器的介紹

JAVA垃圾回收器的介紹

JAVA垃圾回收器的介紹

垃圾回收器一共有7種:

如下圖

image60bb14bf3d3ed85b.png

其中G1 和 CMS 屬於比較新的,暫停時間比之前較少。

serial

serial Old

parNew 這個是serial的多執行緒版本。

Parallel Scavenge

parallel old 這個是Parallel Scavenge的老年代版本。

CMS(concurrent mark sweep)

G1(Gabage first)

CMS回收器和G1回收器的對比

1 CMS回收器

並行回收器之後就是CMS回收器了(concurrent-mark-sweep)。這個演算法使用了多個執行緒(concurrent)來掃描堆並標記(mark)那些不再使用的可以回收(sweep)的物件。這個演算法在兩種情況下會進入一個”stop the world”的模式:當進行根物件的初始標記的時候 (老生代中執行緒入口點或靜態變數可達的那些物件)以及當這個演算法在併發執行的時候應用程式改變了堆的狀態使得它不得不回去再次確認自己標記的物件都是正確的。

使用這個回收器最大的問題就是會碰到promotion failure,這是指在回收新生代及年老代時出現了競爭條件的情況。如果回收器需要將年輕的物件提升到年老代中,而這個時候年老代沒有多餘的空間了,它就只能先進行一次STW(Stop The World)的full GC了——這種情況正是CMS所希望避免的。為了確保這種情況不會發生,你要麼就是增加老生代的大小(或者增加整個堆的大小),要麼就是給回收器分配一些後臺執行緒以便與物件分配的速度進行賽跑。

這個演算法的另一個缺點就是和並行回收器相比,它使用的CPU資源會更多,它使用了多個執行緒來執行掃描和回收,這樣才能讓應用持續提供更高級別的吞吐量。對於大多數長期執行的程式而言,應用的暫停對它們是很不利的,這個時候可以考慮使用CMS回收器。儘管如此,這個演算法也不是預設開啟的。你得指定XX:+UseConcMarkSweepGC來啟用它。假設你的堆小於4G,而你又希望分配更多的CPU資源以避免應用暫停,那麼這就是你要選擇的回收器。然而,如果堆大於4G的話,你可能更希望使用最後的這個——G1回收器。

2 G1回收器

G1( Garbage first)回收器在JDK 7update 4中首次引入,它的設計目標是能更好地支援大於4GB的堆。G1回收器將堆分為多個區域,大小從1MB到32MB不等,並使用多個後臺執行緒來掃描它們。G1回收器會優先掃描那些包含垃圾最多的區域,這正是它的名字的由來(Garbage first)。這個回收器可以通過-XX:UseG1GC標記來啟用。

這一策略減少了後臺執行緒還未掃描完無用物件前堆就已經用光的可能性,而那種情況回收器就必須得暫停應用,這就會導致STW回收。G1的另一個好處就是它總是會進行堆的壓縮,而CMS回收器只有在full GC的時候才會幹這事。

過去幾年裡,大堆一直都是一個充滿爭議的領域,很多開發人員從單機器單JVM模型轉向了單機器多JVM的微服務,元件化的架構。這是許多因素所驅動的,包括隔離程式的元件,簡化部署,避免重新載入應用類到記憶體所產生的開銷(Java 8中這點已經得到了改善)。

儘管如此,這麼做最主要還是希望能避免大堆的GC中長時期的”stop the world”的暫停(在一次大的回收中需要花費數秒才能完成)。像Docker這樣的容器技術也加速了這一程序,它們使得你可以很輕鬆地在同一臺物理機上部署多個應用。

Serial

Serial收集器是最古老的收集器,它的缺點是當Serial收集器想進行垃圾回收的時候,必須暫停使用者的所有程序,即stop the world。到現在為止,它依然是虛擬機器執行在client模式下的預設新生代收集器,與其他收集器相比,對於限定在單個CPU的執行環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾回收自然可以獲得最高的單執行緒收集效率。

Serial Old

是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用”標記-整理“演算法。這個收集器的主要意義也是被Client模式下的虛擬機器使用。在Server模式下,它主要還有兩大用途:一個是在JDK1.5及以前的版本中與Parallel Scanvenge收集器搭配使用,另外一個就是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure的時候使用。

通過指定-UseSerialGC引數,使用Serial + Serial Old的序列收集器組合進行記憶體回收。

ParNew收集器

ParNew收集器是Serial收集器新生代的多執行緒實現,注意在進行垃圾回收的時候依然會stop the world,只是相比較Serial收集器而言它會執行多條程序進行垃圾回收。

ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於存線上程互動的開銷,該收集器在通過超執行緒技術實現的兩個CPU的環境中都不能百分之百的保證能超越Serial收集器。當然,隨著可以使用的CPU的數量增加,它對於GC時系統資源的利用還是很有好處的。它預設開啟的收集執行緒數與CPU的數量相同,在CPU非常多(譬如32個,現在CPU動輒4核加超執行緒,伺服器超過32個邏輯CPU的情況越來越多了)的環境下,可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數。

-UseParNewGC: 開啟此開關後,使用ParNew + Serial Old的收集器組合進行記憶體回收,這樣新生代使用並行收集器,老年代使用序列收集器。

Parallel Scavenge收集器

Parallel是採用複製演算法的多執行緒新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一個特點是它所關注的目標是吞吐量(Throughput)。所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量=執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間)。停頓時間越短就越適合需要與使用者互動的程式,良好的響應速度能夠提升使用者的體驗;而高吞吐量則可以最高效率地利用CPU時間,儘快地完成程式的運算任務。

主要適合在後臺運算而不需要太多互動的任務。

Parallel Old收集器是

Parallel Scavenge收集器的老年代版本,採用多執行緒和”標記-整理”演算法。這個收集器是在jdk1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。原因是如果新生代Parallel Scavenge收集器,那麼老年代除了Serial Old(PS MarkSweep)收集器外別無選擇。由於單執行緒的老年代Serial Old收集器在服務端應用效能上的”拖累“,即使使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,又因為老年代收集中無法充分利用伺服器多CPU的處理能力,在老年代很大而且硬體比較高階的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合”給力“。直到Parallel Old收集器出現後,”吞吐量優先“收集器終於有了比較名副其實的應用祝賀,在注重吞吐量及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。

-UseParallelGC: 虛擬機器執行在Server模式下的預設值,開啟此開關後,使用Parallel Scavenge + Serial Old的收集器組合進行記憶體回收。-UseParallelOldGC: 開啟此開關後,使用Parallel Scavenge + Parallel Old的收集器組合進行垃圾回收

CMS收集器

CMS(Concurrent Mark Swep)收集器是一個比較重要的回收器,現在應用非常廣泛,我們重點來看一下,CMS一種獲取最短回收停頓時間為目標的收集器,這使得它很適合用於和使用者互動的業務。從名字(Mark Swep)就可以看出,CMS收集器是基於標記清除演算法實現的。它的收集過程分為四個步驟:

初始標記(initial mark)
併發標記(concurrent mark)
重新標記(remark)
併發清除(concurrent sweep)

注意初始標記和重新標記還是會stop the world,但是在耗費時間更長的併發標記和併發清除兩個階段都可以和使用者程序同時工作。

不過由於CMS收集器是基於標記清除演算法實現的,會導致有大量的空間碎片產生,在為大物件分配記憶體的時候,往往會出現老年代還有很大的空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前開啟一次Full GC。為了解決這個問題,CMS收集器預設提供了一個-XX:+UseCMSCompactAtFullCollection收集開關引數(預設就是開啟的),用於在CMS收集器進行FullGC完開啟記憶體碎片的合併整理過程,記憶體整理的過程是無法併發的,這樣記憶體碎片問題倒是沒有了,不過停頓時間不得不變長。虛擬機器設計者還提供了另外一個引數-XX:CMSFullGCsBeforeCompaction引數用於設定執行多少次不壓縮的FULL GC後跟著來一次帶壓縮的(預設值為0,表示每次進入Full GC時都進行碎片整理)。

不幸的是,它作為老年代的收集器,卻無法與jdk1.4中已經存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms來收集老年代的時候,新生代只能選擇ParNew或Serial收集器中的一個。ParNew收集器是使用-XX:+UseConcMarkSweepGC選項啟用CMS收集器之後的預設新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。

由於CMS收集器現在比較常用,下面我們再額外瞭解一下CMS演算法的幾個常用引數:

UseCMSInitatingOccupancyOnly:表示只在到達閾值的時候,才進行 CMS 回收。
為了減少第二次暫停的時間,通過-XX:+CMSParallelRemarkEnabled開啟並行remark。如果ramark時間還是過長的話,可以開啟-XX:+CMSScavengeBeforeRemark選項,強制remark之前開啟一次minor gc,減少remark的暫停時間,但是在remark之後也立即開始一次minor gc。
CMS預設啟動的回收執行緒數目是(ParallelGCThreads + 3)/4,如果你需要明確設定,可以通過-XX:+ParallelCMSThreads來設定,其中-XX:+ParallelGCThreads代表的年輕代的併發收集執行緒數目。
CMSClassUnloadingEnabled: 允許對類元資料進行回收。
CMSInitatingPermOccupancyFraction:當永久區佔用率達到這一百分比後,啟動 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。
CMSIncrementalMode:使用增量模式,比較適合單 CPU。
UseCMSCompactAtFullCollection引數可以使 CMS 在垃圾收集完成後,進行一次記憶體碎片整理。記憶體碎片的整理並不是併發進行的。
UseFullGCsBeforeCompaction:設定進行多少次 CMS 垃圾回收後,進行一次記憶體壓縮。
一些建議
對於Native Memory:

使用了NIO或者NIO框架(Mina/Netty)
使用了DirectByteBuffer分配位元組緩衝區
使用了MappedByteBuffer做記憶體對映
由於Native Memory只能通過FullGC回收,所以除非你非常清楚這時真的有必要,否則不要輕易呼叫System.gc()。

另外為了防止某些框架中的System.gc呼叫(例如NIO框架、Java RMI),建議在啟動引數中加上-XX:+DisableExplicitGC來禁用顯式GC。這個引數有個巨大的坑,如果你禁用了System.gc(),那麼上面的3種場景下的記憶體就無法回收,可能造成OOM,如果你使用了CMS GC,那麼可以用這個引數替代:-XX:+ExplicitGCInvokesConcurrent。

此外除了CMS的GC,其實其他針對old gen的回收器都會在對old gen回收的同時回收young gen。

G1收集器

G1收集器是一款面向服務端應用的垃圾收集器。HotSpot團隊賦予它的使命是在未來替換掉JDK1.5中釋出的CMS收集器。與其他GC收集器相比,G1具備如下特點:

並行與併發:G1能更充分的利用CPU,多核環境下的硬體優勢來縮短stop the world的停頓時間。
分代收集:和其他收集器一樣,分代的概念在G1中依然存在,不過G1不需要其他的垃圾回收器的配合就可以獨自管理整個GC堆。
空間整合:G1收集器有利於程式長時間執行,分配大物件時不會無法得到連續的空間而提前觸發一次GC。
可預測的非停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
在使用G1收集器時,Java堆的記憶體佈局和其他收集器有很大的差別,它將這個Java堆分為多個大小相等的獨立區域,雖然還保留新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。

雖然G1看起來有很多優點,實際上CMS還是主流。