在前面,咱們已經瞭解了JVM的分代收集,知道JVM垃圾收集在新生代主要採用標記-複製
算法,在老年代主要採用標記-清除
和標記-整理
算法。接下來,咱們看一看JDK默認虛擬機HotSpot的一些垃圾收集器的實現。html
首先來看一下JDK 11以前所有可用的垃圾收集器。java
圖中列出了七種垃圾收集器,連線表示能夠配合使用,所在區域表示它是屬於新生代收集器或是老年代收集器。算法
這裏還標出了垃圾收集器採用的收集算法,G1收集器比較特殊,總體採用標記-整理
算法,局部採用標記-複製
算法,後面再細講。多線程
Serial收集器是最基礎、歷史最悠久的收集器。併發
如同它的名字(串行),它是一個單線程工做的收集器,使用一個處理器或一條收集線程去完成垃圾收集工做。而且進行垃圾收集時,必須暫停其餘全部工做線程,直到垃圾收集結束——這就是所謂的「Stop The World」。jvm
Serial/Serial Old收集器的運行過程如圖:高併發
ParNew收集器實質上是Serial收集器的多線程並行版本,使用多條線程進行垃圾收集。佈局
ParNew收集器的工做過程如圖所示:性能
這裏值得一提的是Par是Parallel(並行)
的縮寫,但須要注意的是,這個並行(Parallel)
僅僅是描述同一時間多條GC線程協同工做,而不是GC線程和用戶線程同時運行。ParNew垃圾收集也是須要Stop The World的。線程
Parallel Scavenge收集器是一款新生代收集器,基於標記-複製算法實現,也可以並行收集。和ParNew有些相似,但Parallel Scavenge主要關注的是垃圾收集的吞吐量。
所謂吞吐量指的是運行用戶代碼的時間與處理器總消耗時間的比值。這個比例越高,證實垃圾收集佔整個程序運行的比例越小。
Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量:
-XX:MaxGCPauseMillis,最大垃圾回收停頓時間。這個參數的原理是空間換時間,收集器會控制新生代的區域大小,從而儘量保證回收少於這個最大停頓時間。簡單的說就是回收的區域越小,那麼耗費的時間也越小。
因此這個參數並非設置得越小越好。設過小的話,新生代空間會過小,從而更頻繁的觸發GC。
-XX:GCTimeRatio,垃圾收集時間與總時間佔比。這個是吞吐量的倒數,原理和MaxGCPauseMillis相同。
因爲與吞吐量關係密切,Parallel Scavenge收集器也常常被稱做「吞吐量優先收集器」。
Serial Old是Serial收集器的老年代版本,它一樣是一個單線程收集器,使用標記-整理算法。
Serial Old收集器的工做過程如圖:
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程併發收集,基於標記-整理算法實現。
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,一樣是老年代的收集齊,採用標記-清除
算法。
CMS收集齊的垃圾收集分爲四步:
初始標記(CMS initial mark)
:單線程運行,須要Stop The World,標記GC Roots能直達的對象。併發標記((CMS concurrent mark)
:無停頓,和用戶線程同時運行,從GC Roots直達對象開始遍歷整個對象圖。從新標記(CMS remark)
:多線程運行,須要Stop The World,標記併發標記階段產生對象。併發清除(CMS concurrent sweep)
:無停頓,和用戶線程同時運行,清理掉標記階段標記的死亡的對象。涉及到了屢次標記的過程,這裏插入一點
三色抽象
的知識。三色抽象用來描述對象在垃圾收集過程當中的狀態。一般白色表明對象未被掃描到,灰色表示對象被掃描到但未被處理,黑色表示對象及其後代已被處理。在CMS的標記和清除過程當中就用到了這種抽象,詳細的能夠查看參考【5】。
Concurrent Mark Sweep收集器運行示意圖以下:
優勢
:CMS最主要的優勢在名字上已經體現出來——併發收集、低停頓。
缺點
:CMS一樣有三個明顯的缺點。
Mark Sweep算法會致使內存碎片比較多
CMS的併發能力比較依賴於CPU資源,併發回收時垃圾收集線程可能會搶佔用戶線程的資源,致使用戶程序性能降低。
併發清除階段,用戶線程依然在運行,會產生所謂的理「浮動垃圾」(Floating Garbage),本次垃圾收集沒法處理浮動垃圾,必須到下一次垃圾收集才能處理。若是浮動垃圾太多,會觸發新的垃圾回收,致使性能下降。
Garbage First(簡稱G1)收集器是垃圾收集器的一個顛覆性的產物,它開創了局部收集的設計思路和基於Region的內存佈局形式。
雖然G1也還是遵循分代收集理論設計的,但其堆內存的佈局與其餘收集器有很是明顯的差別。之前的收集器分代是劃分新生代、老年代、持久代等。
G1把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每個Region均可以根據須要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器可以對扮演不一樣角色的Region採用不一樣的策略去處理。
這樣就避免了收集整個堆,而是按照若干個Region集進行收集,同時維護一個優先級列表,跟蹤各個Region回收的「價值,優先收集價值高的Region。
G1收集器的運行過程大體可劃分爲如下四個步驟:
初始標記(initial mark),標記了從GC Root開始直接關聯可達的對象。STW(Stop the World)執行。
併發標記(concurrent marking),和用戶線程併發執行,從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆裏的對象圖,找出要回收的對象、
最終標記(Remark),STW,標記再併發標記過程當中產生的垃圾。
篩選回收(Live Data Counting And Evacuation),制定回收計劃,選擇多個Region 構成回收集,把回收集中Region的存活對象複製到空的Region中,再清理掉整個舊 Region的所有空間。須要STW。
相比CMS,G1的優勢有不少,能夠指定最大停頓時間、分Region的內存佈局、按收益動態肯定回收集。
只從內存的角度來看,與CMS的「標記-清除」算法不一樣,G1從總體來看是基於「標記-整理」算法實現的收集器,但從局部(兩個Region 之間)上看又是基於「標記-複製」算法實現,不管如何,這兩種算法都意味着G1運做期間不會產生內存空間碎片,垃圾收集完成以後能提供規整的可用內存。
在JDK 11當中,加入了實驗性質的ZGC。它的回收耗時平均不到2毫秒。它是一款低停頓高併發的收集器。
與CMS中的ParNew和G1相似,ZGC也採用標記-複製算法,不過ZGC對該算法作了重大改進:ZGC在標記、轉移和重定位階段幾乎都是併發的,這是ZGC實現停頓時間小於10ms目標的最關鍵緣由。
ZGC雖然在JDK 11還處於實驗階段,但因爲算法與思想是一個很是大的提高,將來前景相信仍是很廣闊的。
垃圾收集器的選擇須要權衡的點仍是比較多的——例如運行應用的基礎設施如何?使用JDK的發行商是什麼?等等……
這裏簡單地列一下上面提到的一些收集器的適用場景:
設置垃圾收集器(組合)的參數以下:
新生代 | 老年代 | JVM 參數 |
---|---|---|
Incremental | Incremental | -Xincgc |
Serial | Serial | -XX:+UseSerialGC |
Parallel Scavenge | Serial | -XX:+UseParallelGC -XX:-UseParallelOldGC |
Parallel New | Serial | N/A |
Serial | Parallel Old | N/A |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New | Parallel Old | N/A |
Serial | CMS | -XX:-UseParNewGC -XX:+UseConcMarkSweepGC |
Parallel Scavenge | CMS | N/A |
Parallel New | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 | -XX:+UseG1GC |
參考:
【1】:周志朋編著《深刻理解Java虛擬機:JVM高級特性與最佳實踐》
【2】:《垃圾回收算法手冊 自動內存管理的藝術》
【3】:Garbage Collection in Java – What is GC and How it Works in the JVM