JAVA垃圾回收器的介绍
JAVA垃圾回收器的介紹
垃圾回收器一共有7種:
如下圖
其中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還是主流。
總結
以上是生活随笔為你收集整理的JAVA垃圾回收器的介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM类加载的过程
- 下一篇: 一图读懂Java架构