JVM(十一)內(nèi)存與垃圾回收|垃圾回收器

本文介紹垃圾回收器。

目錄
?1 GC的分類與性能指標(biāo)
? 2 評估GC的性能指標(biāo)
??2.1 吞吐量
??2.2 暫停時間
?? 2.3 吞吐量VS暫停時間
?3 不同的垃圾回收器概述
?? 3.1 垃圾收集器發(fā)展史
?? 3.2 7款經(jīng)典的垃圾回收器
?? 3.3 7款經(jīng)典的垃圾收集器與垃圾分代之間的關(guān)系
?? 3.4 垃圾收集器的組合關(guān)系
?? 3.5 查看默認的垃圾收集器
?4 Serial回收器:串行回收
?5 ParNew回收器:并行回收
?6 Parallel回收器:吞吐量優(yōu)先
?7 CMS回收器:低延遲
?8 G1回收器:區(qū)域化分代式
?? 8.1 為什么要發(fā)布Garbage First (G1)GC?
?? 8.2 為什么叫做Garbage First (G1)GC?
?? 8.3 優(yōu)勢(特點)
?? 8.4 缺點
?? 8.5 參數(shù)設(shè)置
?? 8.6 G1回收器的常見操作步驟
?? 8.7 適用場景
?? 8.8 region分區(qū)
?? 8.9 G1回收器垃圾回收過程
?? 8.10 記憶集與寫屏障
?? 8.11 G1回收過程詳解
?? 8.12 優(yōu)化建議
?9 垃圾回收器總結(jié)
?10 GC日志分析
?11 垃圾回收器的新發(fā)展


1 GC的分類與性能指標(biāo)


  • 垃圾收集器沒有在規(guī)范中進行過多的規(guī)定,可以由不同的廠商、不同版本的JVM來實現(xiàn)。
  • 由于JDK的版本處于高速迭代過程中,因此Java發(fā)展至今已經(jīng)衍生了眾多的GC版本。
  • 從不同角度分析垃圾收集器,可以將GC分為不同的類型。

按線程數(shù)分,可以分為串行垃圾回收器和并行垃圾回收器

  • 串行回收指的是在同一時間段內(nèi)只允許有一個CPU用于執(zhí)行垃圾回收操作,此時工作線程被暫停,直至垃圾收集工作結(jié)束。
    • 在諸如單CPU處理器或者較小的應(yīng)用內(nèi)存等硬件平臺不是特別優(yōu)越的場合,串行回收器的性能表現(xiàn)可以超過并行回收器和并發(fā)回收器。所以,串行回收默認被應(yīng)用在客戶端的Client模式下的JVM中
    • 在并發(fā)能力比較強的CPU上,并行回收器產(chǎn)生的停頓時間要短于串行回收器。
  • 和串行回收相反,并行收集可以運用多個CPU同時執(zhí)行垃圾回收,因此提升 了應(yīng)用的吞吐量,不過并行回收仍然與串行回收一樣,采用獨占式,使用了“ Stop-the-world”機制。

按照工作模式分,可以分為并發(fā)式垃圾回收器和獨占式垃圾回收器

  • 并發(fā)式垃圾回收器與應(yīng)用程序線程交替工作,以盡可能減少應(yīng)用程序的停頓時間。
  • 獨占式垃圾回收器(Stop the world)一旦運行,就停止應(yīng)用程序中的所有用戶線程,直到垃圾回收過程完全結(jié)束。


按碎片處理方式分,可分為壓縮式垃圾回收器和非壓縮式垃圾回收器

  • 壓縮式垃圾回收器會在回收完成后,對存活對象進行壓縮整理,消除回收后的碎片。
    • 再分配對象空間使用: 指針碰撞
  • 非壓縮式的垃圾回收器不進行這步操作。
    • 再分配對象空間使用: 空閑列表

按工作的內(nèi)存區(qū)間分,又可分為年輕代垃圾回收器和老年代垃圾回收器

2 評估GC的性能指標(biāo)


  • 吞吐量:運行用戶代碼的時間占總運行時間的比例

    • (總運行時間:程序的運行時間十內(nèi)存回收的時間)
  • 垃圾收集開銷:吞吐量的補數(shù),垃圾收集所用時間與總運行時間的比例。

  • 暫停時間:執(zhí)行垃圾收集時,程序的工作線程被暫停的時間

  • 收集頻率:相對于應(yīng)用程序的執(zhí)行,收集操作發(fā)生的頻率。

  • 內(nèi)存占用: Java堆區(qū)所占的內(nèi)存大小

  • 快速:一個對象從誕生到被回收所經(jīng)歷的時間。

  • 這三者(加粗部分)共同構(gòu)成一個“不可能三角”。三者總體的表現(xiàn)會隨著技術(shù)進步而越來越好。一款優(yōu)秀的收集器通常最多同時滿足其中的兩項。

  • 這三項里,暫停時間的重要性日益凸顯。因為隨著硬件發(fā)展,內(nèi)存占用多,硬件性能的提升也有助于降低收集器運行時對應(yīng)用程序的影響,即提高了吞吐量。而內(nèi)存的擴大,對延遲反而帶來負面效果。

  • 簡單來說,主要抓住兩點:

    • 吞吐量
    • 暫停時間

2.1 吞吐量

  • 吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/ (運行用戶代碼時間+垃圾收集時間)
    • 比如:虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%
  • 這種情況下,應(yīng)用程序能容忍較高的暫停時間,因此,高吞吐量的應(yīng)用程序有更長的時間基準(zhǔn),快速響應(yīng)是不必考慮的。
  • 吞吐量優(yōu)先,意味著在單位時間內(nèi),STW的時間最短: 0.2 + 0.2 = 0.4


2.2 暫停時間

  • “暫停時間”是指一個時間段內(nèi)應(yīng)用程序線程暫停,讓GC線程執(zhí)行的狀態(tài)
    • 例如,GC期間100毫秒的暫停時間意味著在這100毫秒期間內(nèi)沒有應(yīng)用程序線程是活動的。.
  • 暫停時間優(yōu)先,意味著盡可能讓單次STW的時間最短: 0.1 + 0.1 + 0.1 + 0.1+0.1=0.5

2.3 吞吐量VS暫停時間

  • 高吞吐量較好因為這會讓應(yīng)用程序的最終用戶感覺只有應(yīng)用程序線程在做“生產(chǎn)性”工作。直覺上,吞吐量越高程序運行越快。
  • 低暫停時間(低延遲)較好因為從最終用戶的角度來看不管是GC還是其他原因?qū)е乱粋€應(yīng)用被掛起始終是不好的。這取決于應(yīng)用程序的類型,有時候甚至短暫的200毫秒暫停都可能打斷終端用戶體驗。因此,具有低的較大暫停時間是非常重要的,特別是對于一個交互式應(yīng)用程序。
  • 不幸的是”高吞吐量”和”低暫停時間”是一對相互競爭的目標(biāo)(矛盾)。
    • 因為如果選擇以吞吐量優(yōu)先,那么必然需要降低內(nèi)存回收的執(zhí)行頻率,但是這樣會導(dǎo)致GC需要更長的暫停時間來執(zhí)行內(nèi)存回收。
    • 相反的,如果選擇以低延遲優(yōu)先為原則,那么為了降低每次執(zhí)行內(nèi)存回收時的暫停時間,也只能頻繁地執(zhí)行內(nèi)存回收,但這又引起了年輕代內(nèi)存的縮誠和導(dǎo)致程序吞吐量的下降。
  • 在設(shè)計(或使用) GC算法時,我們必須確定我們的目標(biāo): 一個GC算法只可能針對兩個目標(biāo)之一(即只專注于較大吞吐量或最小暫停時間),或嘗試找到一個二者的折衷。
  • 現(xiàn)在標(biāo)準(zhǔn):在最大吞吐量優(yōu)先的情況下,降低停頓時間。

3 不同的垃圾回收器概述


3.1 垃圾收集器發(fā)展史

有了虛擬機,就一定需要收集垃圾的機制,這就是Garbage Collection, 對應(yīng)的產(chǎn)品我們稱為Garbage Collector.

  • 1999年隨JDK1.3.1一 起來的是串行方式的Serial GC,它是第一款GC。ParNew垃圾收集器是Serial收集器的多線程版本
  • 2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟隨JDK1.4.2一起發(fā)布
  • Parallel GC在JDK6之后成為HotSpot默認GC。
  • 2012年,在JDK1.7u4版本中,G1可用。
  • 2017年,JDK9中G1變成默認的垃圾收集器,以替代CMS。
  • 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,實現(xiàn)并行性來改善最壞情況下的延遲。
  • 2018年9月,JDK11發(fā)布。引入Epsilon垃圾回收器,又被稱為"No-op (無操作) "回收器。同時,引入ZGC:可伸縮的低延遲垃圾回收器(Experimental)。
  • 2019年3月,JDK12發(fā)布。 增強G1,自動返回未用堆內(nèi)存給操作系統(tǒng)。同時,引入Shenandoah GC:低停頓時間的GC (Experimental)。
  • 2019年9月,JDK13發(fā)布。增強ZGC,自動返回未用堆內(nèi)存給操作系統(tǒng)。
  • 2020年3月,JDK14發(fā)布。刪除CMS垃圾回收器。擴展ZGC在macOS和Windows上的應(yīng)用

3.2 7款經(jīng)典的垃圾收集器

  • 串行回收器:Serial. Serial Old

  • 并行回收器:ParNew. Parallel Scavenge. Parallel Old

  • 并發(fā)回收器:CMS. G1

3.3 7款經(jīng)典的垃圾收集器與垃圾分代之間的關(guān)系

  • 新生代收集器: Serial、 ParNeW、Parallel Scavenge;

  • 老年代收集器: Serial 0ld、 Parallel 0ld、 CMS;

  • 整堆收集器: G1;


3.4 垃圾收集器的組合關(guān)系

  • 兩個收集器間有連線,表明它們可以搭配使用: Serial/Serial old、Serial/CMS、 ParNew/Serial old、ParNew/CMS、 Parallel Scavenge/Serial old、Parallel Scavenge/Parallel old、G1;
  • 其中Serial old作為CMS 出現(xiàn)"Concurrent Mode Failure"失敗的后 備預(yù)案。
  • (紅色虛線)由于維護和兼容性測試的成本,在JDK 8時將Serial+CMS、 ParNew+Serial old這兩個組合聲明為廢棄(JEP 173),并在JDK 9中完全取消了這些組合的支持(JEP214),即:移除。
  • (綠色虛線)JDK 14中:棄用Parallel Scavenge和Serial old GC組合(JEP366 )
  • (青色虛線)JDK 14中:刪除CMS垃圾回收器 (JEP 363)

?? 為什么要有很多收集器個不夠嗎? 因為Java的使用場景很多, 移動端,服務(wù)器等。所以就需要針對不同的場景,提供不同的垃圾收集器,提高垃圾收集的性能。
??雖然我們會對各個收集器進行比較,但并非為了挑選一個最好的收集器出來。沒有一種放之四海皆準(zhǔn)、任何場景下都適用的完美收集器存在,更加沒有萬能的收集器。所以我們選擇的只是對具體應(yīng)用最合適的收集器。

3.5 查看默認的垃圾收集器

  • -xx:+PrintCommandLineFlags: 查看命令行相關(guān)參數(shù)(包含使用的垃圾收集器)
/**
 *  -XX:+PrintCommandLineFlags 
 *
 * 修改默認GC
 *  -XX:+UseSerialGC:表明新生代使用Serial GC ,同時老年代使用Serial Old GC
 *
 *  -XX:+UseParNewGC:標(biāo)明新生代使用ParNew GC
 *
 *  -XX:+UseParallelGC:表明新生代使用Parallel GC
 *  -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC
 *  說明:二者可以相互激活
 *
 *  -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同時,年輕代會觸發(fā)對ParNew 的使用
 */
public class GCUseTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();

        while(true){
            byte[] arr = new byte[100];
            list.add(arr);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 使用命令行指令:jinfo 一flag相關(guān)垃圾回收器參數(shù)進程ID
    jdk8 默認使用的是parallel

4 Serial回收器:串行回收


  • Serial收集器是最基本、歷史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的選擇。
  • Serial收集器作為HotSpot中Client模式下的默認新生代垃圾收集器。
  • Serial收集器采用復(fù)制算法、串行回收Stop-the-World機制的方式執(zhí)行內(nèi)存回收。
  • 除了年輕代之外,Serial收集器還提供用于執(zhí)行老年代垃圾收集的Serial old收集器。 Serial old收集器同樣也采用了串行回收Stop the World機制,只不過內(nèi)存回收算法使用的是標(biāo)記-壓縮算法。
    • Serial Old是運行在Client模式下默認的老年代的垃圾回收器
    • Serial Old在Server模式下主要有兩個用途:①與新生代的ParallelScavenge配合使用; ②作為老年代CMS收集器的后備垃圾收集方案
  • 這個收集器是一個單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結(jié)束(Stop The World )。

優(yōu)勢

  • 簡單而高效(與其他收集器的單線程比),對于限定單個CPU的環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。
    • 運行在Client模式下的虛擬機是個不錯的選擇。
  • 在用戶的桌面應(yīng)用場景中,可用內(nèi)存一般不大(幾十MB至一兩百MB), 可以在較短時間內(nèi)完成垃圾收集(幾十ms至一百多ms) ,只要不頻繁發(fā)生,使用串行回收器是可以接受的。
  • 在HotSpot虛擬機中,使用-XX:+UseSerialGC 參數(shù)可以指定年輕代和老年代都使用串行收集器。
    • 等價于新生代用Serial GC,且老年代用Serial Old GC

總結(jié)

  • 這種垃圾收集器了解即可,現(xiàn)在已經(jīng)不用串行的了。而且它限定單核cpu才可以用?,F(xiàn)在都不是單核的了。
  • 對于交互較強的應(yīng)用而言,這種垃圾收集器是不能接受的。一般在Javaweb應(yīng)用程序中是不會采用串行垃圾收集器的。

5 ParNew回收器:并行回收


  • 如果說Serial GC是年輕代中的單線程垃圾收集器,那么ParNew收集器則是Serial收集器的多線程版本。

    • Par是Parallel的縮寫,New: 只能處理的是新生代
  • ParNew收集器除了采用并行回收的方式執(zhí)行內(nèi)存回收外,兩款垃圾收集器之間幾乎沒有任何區(qū)別。ParNew收集器在年輕代中同樣也是采用復(fù)制算法、Stop-the-World機制。

  • ParNew是很多JVM運行在Server模式下新生代的默認垃圾收集器。

    • 對于新生代,回收次數(shù)頻繁,使用并行方式高效。
    • 對于老年代,回收次數(shù)少,使用串行方式節(jié)省資源。(CPU并行 需要切換線程,串行可以省去切換線程的資源)
  • 由于ParNew收集器是基于并行回收,那么是否可以斷定ParNew收集器的回收效率在任何場景下都會比Serial收集器更高效?

    • ParNew 收集器運行在多CPU的環(huán)境下,由于可以充分利用多CPU、 多核心等物理硬件資源優(yōu)勢,可以更快速地完成垃圾收集,提升程序的吞吐量。
    • 但是在單個CPU的環(huán)境下,ParNew收集器不比Serial收集器更高效。雖然Serial收集器是基于串行回收,但是由于CPU不需要頻繁地做任務(wù)切換,因此可以有效避免多線程交互過程中產(chǎn)生的一些額外開銷。
  • 除Serial外,目前只有ParNew GC能與CMS收集器配合工作

  • 在程序中,開發(fā)人員可以通過選項-XX:+UseParNewGC手動指定使用ParNew收集器執(zhí)行內(nèi)存回收任務(wù)。它表示年輕代使用并行收集器,不影響老年代。

  • -XX:ParallelGCThreads 限制線程數(shù)量,默認開啟和CPU數(shù)據(jù)相同的線程數(shù)。.

6 Parallel回收器:吞吐量優(yōu)先


  • HotSpot的年輕代中除了擁有ParNew收集器是基于并行回收的以外, Parallel Scavenge收集器同樣也采用了復(fù)制算法、并行回收Stop the World機制。
  • 那么Parallel收集器的出現(xiàn)是否多此一舉?
    • 和ParNew收集器不同,Parallel Scavenge收集 器的目標(biāo)則是達到一個可控制的吞吐量(Throughput),它也被稱為吞吐量優(yōu)先的垃圾收集器。
    • 自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge 與ParNew一個重要區(qū)別。
  • 高吞吐量則可以高效率地利用CPU 時間,盡快完成程序的運算任務(wù),主要適合在后臺運算而不需要太多交互的任務(wù)。因此,常見在服務(wù)器環(huán)境中使用。例如,那些執(zhí)行批量處理、訂單處理、工資支付、科學(xué)計算的應(yīng)用程序。
  • Parallel收集器在JDK1.6時提供了用于執(zhí)行老年代垃圾收集的 Parallel Old收集器,用來代替老年代的Serial Old收集器。
  • Parallel Old收集器采用了標(biāo)記-壓縮算法,但同樣也是基于并行回收Stop-the-World機制。
  • 在程序吞吐量優(yōu)先的應(yīng)用場景中,Parallel 收集器和Parallel Old收集器的組合,在Server模式下的內(nèi)存回收性能很不錯。
  • 在Java8中,默認是此垃圾收集器

參數(shù)配置

  • -XX:+UseParallelGC手動指定 年輕代使用Parallel并行收集器執(zhí)行內(nèi)存回收任務(wù)。
  • -XX:+UseParallelOldGc手動指定老年代都是使用并行回收收集器。
    • 分別適用于新生代和老年代。默認jdk8是開啟的。
    • 上面兩個參數(shù),默認開啟一個,另一個也會被開啟。 (互相激活)
  • -XX:ParallelGCThreads設(shè)置年輕代并行收集器的線程數(shù)。一般地,最好與CPU數(shù)量相等,以避免過多的線程數(shù)影響垃圾收集性能。
    • 在默認情況下,當(dāng)CPU數(shù)量小于8個, Paralle lGCThreads 的值等于CPU數(shù)量。
    • 當(dāng)CPU數(shù)量大于8個, ParallelGCThreads的值等于3+[5*CPU_ Count]/8]
  • -XX :MaxGCPauseMillis設(shè)置垃圾收集器最大停頓時間(即STW的時間)。單位是毫秒。
    • 為了盡可能地把停頓時間控制在MaxGCPauseMills以內(nèi),收集器在.工作時會調(diào)整Java堆大小或者其他一些參數(shù)。
    • 對于用戶來講,停頓時間越短體驗越好。但是在服務(wù)器端,我們注重 高并發(fā),整體的吞吐量。所以服務(wù)器端適合Parallel進行控制。該參數(shù)使用需謹(jǐn)慎。
  • -XX:GCTimeRatio垃圾收集時間占總時間的比例(= 1 / (N + 1))用于衡量吞吐量的大小。
    • 取值范圍(0, 100)。默認值99。
    • 與前一個-XX:MaxGCPauseMillis參數(shù)有一定矛盾性。暫停時間越長,Radio參數(shù)就容易超過設(shè)定的比例。
  • -XX:+UseAdaptiveSizePolicy設(shè) 置Parallel Scavenge收集器具有自適應(yīng)調(diào)節(jié)策略
    • 在這種模式下,年輕代的大小、Eden和Survivor的比例、晉升老年 代的對象年齡等參數(shù)會被自動調(diào)整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。
    • 在手動調(diào)優(yōu)比較困難的場合,可以直接使用這種自適應(yīng)的方式,僅指 定虛擬機的最大堆、目標(biāo)的吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMills),讓虛擬機自己完成調(diào)優(yōu)工作。

7 CMS回收器:低延遲


  • 在JDK1.5時期, HotSpot推出了一款在強交互應(yīng)用中幾乎可認為有劃時代意義的垃圾收集器: CMS (Concurrent -Mark- Sweep)收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的并發(fā)收集器,它第一次實現(xiàn)了讓垃圾收集線程與用戶線程同時工作。
  • CMS收集器的關(guān)注點是盡可能縮短垃圾收集時用戶線程的停頓時間。停頓時 間越短(低延遲)就越適合與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗。
    • 目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應(yīng)用的需求。
  • CMS的垃圾收集算法采用標(biāo)記-清除算法,并且也 會stop-the-world
  • 不幸的是,CMS 作為老年代的收集器,卻無法與JDK 1.4.0 中已經(jīng)存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1. 5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。
  • 在G1出現(xiàn)之前,CMS使用還是非常廣泛的。一直到今天,仍然有很多系統(tǒng)使用CMS GC。

垃圾收集過程

??CMS整個過程比之前的收集器要復(fù)雜,整個過程分為4個主要階段,即初始標(biāo)記階段、并發(fā)標(biāo)記階段、重新標(biāo)記階段并發(fā)清除階段。

  • 初始標(biāo)記(Initial-Mark) 階段:在這個階段中,程序中所有的工作線程都將會因為. “Stop-the-World"機制而出現(xiàn)短暫的暫停,這個階段的主要任務(wù)僅僅只是標(biāo)記出GCRoots能直接關(guān)聯(lián)到的對象。一旦標(biāo)記完成之后就會恢復(fù)之前被暫停的所有應(yīng)用.線程。由于直接關(guān)聯(lián)對象比較小,所以這里的速度非??臁?/li>
  • 并發(fā)標(biāo)記(Concurrent-Mark)階段:從GC Roots的 直接關(guān)聯(lián)對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發(fā)運行。
  • 重新標(biāo)記(Remark) 階段:由于在并發(fā)標(biāo)記階段中,程序的工作線程會和垃圾收集線程同時運行或者交叉運行,因此為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,這個階段的停頓時間通常會比初始標(biāo)記階段稍長一些,但也遠比并發(fā)標(biāo)記階段的時間短。
  • 并發(fā)清除( Concurrent-Sweep)階段:此階段清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對象,釋放內(nèi)存空間。由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發(fā)的

??盡管CMS收集器采用的是并發(fā)回收(非獨占式),但是在其初始化標(biāo)記和再次標(biāo)記這兩個階段中仍然需要執(zhí)行“Stop-the-World”機制暫停程序中的工作線程,不過暫停時間并不會太長,因此可以說明目前所有的垃圾收集器都做不到完全不需要“Stop-the-World”,只是盡可能地縮短暫停時間
??由于最耗費時間的并發(fā)標(biāo)記與并發(fā)清除階段都不需要暫停工作,所以整體的回收是低停頓的。
??另外,由于在垃圾收集階段用戶線程沒有中斷,所以在CMS回收過程中,還應(yīng)該確保應(yīng)用程序用戶線程有足夠的內(nèi)存可用。因此,CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,而是當(dāng)堆內(nèi)存使用率達到某一閾值時,便開始進行回收,以確保應(yīng)用程序在CMS工作過程中依然有足夠的空間支持應(yīng)用程序運行。要是CMS運行期間預(yù)留的內(nèi)存無法滿足程序需要,就會出現(xiàn)一次“Concurrent Mode Failure”失敗,這時虛擬機將啟動后備預(yù)案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。
??CMS收集器的垃圾收集算法采用的是標(biāo)記--清除算法,這意味著每次執(zhí)行完內(nèi)存回收后,由于被執(zhí)行內(nèi)存回收的無用對象所占用的內(nèi)存空間極有可能是不連續(xù)的一些內(nèi)存塊,不可避免地將會產(chǎn)生一些內(nèi)存碎片。 那么CMS在為新對象分配內(nèi)存空間時,將無法使用指針碰撞(Bump the Pointer) 技術(shù),而只能夠選擇空閑列表(Free List) 執(zhí)行內(nèi)存分配。

有人會覺得既然Mark Sweep會造成內(nèi)存碎片,那么為什么不把算法換成Mark Compact呢?
因為當(dāng)并發(fā)清除的時候,用Compact整理內(nèi)存的話,原來的用戶線程使用的內(nèi)存還怎么用呢?要保證用戶線程能繼續(xù)執(zhí)行,前提的它運行的資源不受影響嘛。Mark Compact更適合“Stop the World”這種場景”下使用

CMS的優(yōu)點 .

  • 并發(fā)收集
  • 低延遲

CMS的弊端

  • 會產(chǎn)生內(nèi)存碎片,導(dǎo)致并發(fā)清除后,用戶線程可用的空間不足。在無法分配大對象的情況下,不得不提前觸發(fā)Full GC。
  • CMS收集器對CPU資源非常敏感。在并發(fā)階段,它雖然不會導(dǎo)致用戶停頓,但是會因為占用了一部分線程而導(dǎo)致應(yīng)用程序變慢,總吞吐量會降低。
  • CMS收集器無法處理浮動垃圾。# CMS垃圾收集器——重新標(biāo)記和浮動垃圾的思考

參數(shù)設(shè)置

  • -XX:+UseConcMarkSweepGc 手動指定使用CMS收集器執(zhí)行內(nèi)存回收任務(wù)。
    • 開啟該參數(shù)后會自動將-XX:+UseParNewGc打開。即: ParNew (Young區(qū)用) +CMS (Old區(qū)用) +Serial Old的組合。
  • -XX:CMS1ni tiatingOccupanyFraction設(shè)置堆內(nèi)存使用率的閾值,一旦達到該閾值,便開始進行回收。
    • JDK5及以前版本的默認值為68,即當(dāng)老年代的空間使用率達到68%時,會執(zhí)行一 次CMS 回收。 JDK6及以上版本默認值為92%
    • 如果內(nèi)存增長緩慢,則可以設(shè)置一個稍大的值,大的閾值可以有效降低CMS的觸發(fā)頻率,減少老年代回收的次數(shù)可以較為明顯地改善應(yīng)用程序性能。反之,如果應(yīng)用程序內(nèi)存使用率增長很快,則應(yīng)該降低這個閾值,以避免頻繁觸發(fā)老年代串行收集器。因此通過該選項便可以有效降低Full GC的執(zhí)行次數(shù)。
  • -XX:+UseCMSCompactAtFullCollection用于指定在執(zhí)行完Full GC后對內(nèi)存空間進行壓縮整理,以此避免內(nèi)存碎片的產(chǎn)生。不過由于內(nèi)存壓縮整理過程無法并發(fā)執(zhí)行,所帶來的問題就是停頓時間變得更長了。
  • -XX:CMSFullGCsBeforeCompaction設(shè)置在執(zhí)行多少次Full GC后對內(nèi)存空間進行壓縮整理。
  • -XX:ParallelCMSThreads 設(shè)置CMS的線程數(shù)量。
    • CMS 默認啟動的線程數(shù)是(ParallelGCThreads+3) /4, ParallelGCThreads是年輕代并行收集器的線程數(shù)。當(dāng)CPU資源比較緊張時,受到CMs收集器線程的影響,應(yīng)用程序的性能在垃圾回收階段可能會非常糟糕。

小結(jié)
如果你想要最小化地使用內(nèi)存和并行開銷,請選Serial GC;
如果你想要最大化應(yīng)用程序的吞吐量,請選Parallel GC;
如果你想要最小化GC的中斷或停頓時間,請選CMS GC。

JDK 后續(xù)版本中CMS的變化

  • JDK9新特性: CMS被標(biāo)記為Deprecate了(JEP291)
    • 如果對JDK 9及以上版本的HotSpot虛擬機使用參數(shù)-XX:+UseConcMarkSweepGC來開啟CMS收集器的話,用戶會收到一個警告信息,提示CMS未來將會被廢棄。
  • JDK14新特性: 刪除CMS垃圾回收器(JEP363)
    • 移除了CMS垃圾收集器,如果在JDK14中使用-XX: +UseConcMarkSweepGC的話,JVM不會報錯,只是給出一個warning信息,但是不會exit。JVM會自動回退以默認GC方式啟動JVM

8 G1回收器:區(qū)域化分代式


  • G1 (Garbage-First) 是一款面向服務(wù)端應(yīng)用的垃圾收集器,主要針對配備多核CPU及大容量內(nèi)存的機器,以極高概率滿足GC停頓時間的同時,還兼具高吞吐量的性能特征。
  • 在JDK1. 7版本正式啟用,移除了Experimental的標(biāo)識,是JDK 9以后的默認垃圾回收器,取代了CMS回收器及Parallel + Parallel Old組合。被Oracle官方稱為“全功能的垃圾收集器” 。
  • 與此同時,CMS已經(jīng)在JDK 9中被標(biāo)記為廢棄(deprecated) 。G1在jdk8中還不是默認的垃圾回收器,需要使用-XX:+UseG1GC來啟用。

8.1 為什么要發(fā)布Garbage First (G1)GC?

??原因就在于應(yīng)用程序所應(yīng)對的業(yè)務(wù)越來越龐大、復(fù)雜,用戶越來越多,沒有GC就不能保證應(yīng)用程序正常進行,而經(jīng)常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行優(yōu)化。G1(Garbage-First) 垃圾回收器是在Java7 update4之后引入的一個新的垃圾回收器,是當(dāng)今收集器技術(shù)發(fā)展的最前沿成果之一。
??與此同時,為了適應(yīng)現(xiàn)在不斷擴大的內(nèi)存和不斷增加的處理器數(shù)量,進一步降低暫停時間(pause time) ,同時兼顧良好的吞吐量,官方給G1設(shè)定的目標(biāo)是在延遲可控的情況下獲得盡可能高的吞吐量,所以才擔(dān)當(dāng)起“全功能收集器”的重任與期望

8.2 為什么叫做Garbage First (G1)呢?

  • 因為G1是一個并行回收器,它把堆內(nèi)存分割為很多不相關(guān)的區(qū)域(Region) (物理上 不連續(xù)的)。使用不同的Region來表示Eden、幸存者0區(qū),幸存者1區(qū),老年代等。
  • G1 GC有計劃地避免在整個Java 堆中進行全區(qū)域的垃圾收集。G1跟蹤各個Region 里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region。
  • 由于這種方式的側(cè)重點在于回收垃圾最大量的區(qū)間(Region),所以我們給G1一個名字:垃圾優(yōu)先(Garbage First)

8.3 優(yōu)勢(特點)

與其他GC收集器相比,G1使用了全新的分區(qū)算法,其特點如下所示:

  • 并行與并發(fā)

    • 并行性: G1在回收期間,可以有多個GC線程同時工作,有效利用多核計算能力。此時用戶線程STW
    • 并發(fā)性: G1擁有與應(yīng)用程序交替執(zhí)行的能力,部分工作可以和應(yīng)用程序同時執(zhí)行,因此,一般來說,不會在整個回收階段發(fā)生完全阻塞應(yīng)用程序的情況
  • 分代收集

    • 從分代上看,G1依然屬于分代型垃圾回收器,它會區(qū)分年輕代和老年代,年輕代依然有Eden區(qū)和Survivor區(qū)。但從堆的結(jié)構(gòu)上看,它不要求整個Eden區(qū)、年輕代或者老年代都是連續(xù)的,也不再堅持固定大小和固定數(shù)量。


      不要求堆結(jié)構(gòu)連續(xù)
    • 將堆空間分為若干個區(qū)域(Region) ,這些區(qū)域中包含了邏輯上的年輕代和老年代。

    • 和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;


  • 空間整合

    • G1將內(nèi)存劃分為一個個的region。 內(nèi)存的回收是以region作為基本單的。Region之間是復(fù)制算法,但整體上實際可看作是標(biāo)記-壓縮(Mark-Compact)算法,兩種算法都可以避免內(nèi)存碎片。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC。尤其是當(dāng)Java堆非常大的時候,G1的優(yōu)勢更加明顯。
  • 可預(yù)測的停頓時間模型(即:軟實時soft real-time)

    • 這是G1相對于CMS的另一大優(yōu)勢,G1除了追求低停頓外,還能建立可預(yù)測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒。
    • 由于分區(qū)的原因,G1可以只選取部分區(qū)域進行內(nèi)存回收,這樣縮小了回收的范圍,因此對于全局停頓情況的發(fā)生也能得到較好的控制。
    • G1跟蹤各個Region里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以 及回收所需時間的經(jīng)驗值),在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region。保證了G1 收集器在有限的時間內(nèi)可以獲取盡可能高的收集效率。
    • 相比于CMS GC,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多。
  • HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用內(nèi)置的JVM線程執(zhí)行 GC的多線程操作,而G1 GC可以采用應(yīng)用線程承擔(dān)后臺運行的GC工作,即當(dāng)JVM的GC線程處理速度慢時,系統(tǒng)會調(diào)用應(yīng)用程序線程幫助加速垃圾回收過程。

8.4 缺點

  • 相較于CMS,G1還不具備全方位、壓倒性優(yōu)勢。比如在用戶程序運行過程中,G1無論是為了垃圾收集產(chǎn)生的內(nèi)存占用(Footprint) 還是程序運行時的額外執(zhí)行負載(overload) 都要比CMS要高。
  • 從經(jīng)驗上來說,在小內(nèi)存應(yīng)用上CMS的表現(xiàn)大概率會優(yōu)于G1,而G1在大內(nèi)存應(yīng)用上則發(fā)揮其優(yōu)勢。平衡點在6~8GB之間。

8.5 參數(shù)設(shè)置

  • -XX:+UseG1GC 手動指定使用G1收集器執(zhí)行內(nèi)存回收任務(wù)。
  • -XX:G1HeapRegionSize 設(shè)置每個Region的大小。值是2的冪,范圍是1MB 到32MB之間,目標(biāo)是根據(jù)最小的Java堆大小劃分出約2048個區(qū)域。默認是堆內(nèi)存的1/2000。
  • -XX:MaxGCPauseMillis 設(shè)置期望達到的最大Gc停頓時間指標(biāo)(JVM會盡力實現(xiàn),但不保證達到)。默認值是200ms
  • -XX:ParallelGCThread 設(shè)置STW工作線程數(shù)的值。最多設(shè)置為8
  • -XX:ConcGCThreads 設(shè)置并發(fā)標(biāo)記的線程數(shù)。將n設(shè)置為并行垃圾回收線程數(shù)(ParallelGCThreads)的1/4左右。
  • -XX:InitiatingHeapOccupancyPercent 設(shè)置觸發(fā)并發(fā)GC周期的Java堆占用率閾值。超過此值,就觸發(fā)GC。默認值是45。

8.6 G1回收器的常見操作步驟

G1的設(shè)計原則就是簡化JVM性能調(diào)優(yōu),開發(fā)人員只需要簡單的三步即可完成調(diào)優(yōu):

  • 第一步:開啟G1垃圾收集器
  • 第二步:設(shè)置堆的最大內(nèi)存
  • 第三步:設(shè)置最大的停頓時間

G1中提供了三種垃圾回收模式: YoungGC、 Mixed GC和Full GC, 在不同的條件下被觸發(fā)。

8.7 適用場景

  • 面向服務(wù)端應(yīng)用,針對具有大內(nèi)存、多處理器的機器。(在普通大小的堆里表現(xiàn)并不驚喜)
  • 最主要的應(yīng)用是需要低GC延遲,并具有大堆的應(yīng)用程序提供解決方案;
  • 如:在堆大小約6GB或更大時,可預(yù)測的暫停時間可以低于0.5秒; ( G1通過每次只清理一部分而不是全部的Region的增量式清理來保證每次GC停頓時間不會過長)。
  • 用來替換掉JDK1.5中的CMS收集器; 在下面的情況時,使用G1可能比CMS好:
    ①超過50%的Java堆被活動數(shù)據(jù)占用;
    ②對象分配頻率或年代提升頻率變化很大;
    ③GC停頓時間過長(長于0. 5至1秒)。

8.8 region分區(qū)

??使用G1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region塊,每個Region塊大小根據(jù)堆空間的實際大小而定,整體被控制在1MB到32MB之間,且為2的N次冪,即1MB, 2MB, 4MB, 8MB, 1 6MB, 32MB??梢酝ㄟ^ -XX:G1HeapRegionSize設(shè)定。所有的Region大小相同,且在JVM生命周期內(nèi)不會被改變。
??雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region (不需要連續(xù))的集合。通過Region的動態(tài)分配方式實現(xiàn)邏輯上的連續(xù)。


  • 一個region 有可能屬于Eden, Survivor 或者Old/Tenured 內(nèi)存區(qū)域。但是一個region只可能屬于一個角色。圖中的E表示該region屬于Eden內(nèi)存區(qū)域,s表示屬于Survivor內(nèi)存區(qū)域,O表示屬于Old內(nèi)存區(qū)域。圖中空白的表示未使用的內(nèi)存空間。
  • G1垃圾收集器還增加了一種新的內(nèi)存區(qū)域,叫做Humongous內(nèi)存區(qū)域,如圖中的H塊。主要用于存儲大對象,如果超過1. 5個region,就放到H。
  • 設(shè)置H的原因:
    • 對于堆中的大對象,默認直接會被分配到老年代,但是如果它是一個短期存在的大對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區(qū),它用來專門存放大對象。如果一個H區(qū)裝不下一個大對象,那么G1會尋找連續(xù)的H區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時候不得不啟動Full GC。G1的大多數(shù)行為都把H區(qū)作為老年代的一部分來看待。

8.9 G1回收器垃圾回收過程

G1 GC的垃圾回收過程主要包括如下三個環(huán)節(jié):

  • 年輕代GC (Young GC )

  • 老年代并發(fā)標(biāo)記過程( Concurrent Marking)

  • 混合回收(Mixed GC )

  • (如果需要,單線程、獨占式、高強度的Full GC還是繼續(xù)存在的。它針對GC的評估失敗提供了一種失敗保護機制,即強力回收。)

    順時針, young gc 一> young gc + concurrent mark 一> Mixed GC順序,進行垃圾回收。

  • 應(yīng)用程序分配內(nèi)存,當(dāng)年輕代的Eden區(qū)用盡時開始年輕代回收過程; G1的年輕代收集階段是一個并行的獨占式收集器。在年輕代回收期,G1 GC暫停所有應(yīng)用程序線程,啟動多線程執(zhí)行年輕代回收。然后從年輕代區(qū)間移動存活對象到Survivor區(qū)間或者老年區(qū)間,也有可能是兩個區(qū)間都會涉及。

  • 當(dāng)堆內(nèi)存使用達到一定值(默認45%)時,開始老年代并發(fā)標(biāo)記過程。

  • 標(biāo)記完成馬上開始混合回收過程。對于一個混合回收期,G1 GC從老年區(qū)間移動存活對象到空閑區(qū)間,這些空閑區(qū)間也就成為了老年代的一部分。和年輕代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整個老年代被回收,一次只需要掃描/回收一小部分老年代的Region就可以了。同時,這個老年代Region是和年輕代一起 被回收的。

  • 舉個例子:一個web服務(wù)器,Java進程最大堆內(nèi)存為4G,每分鐘響應(yīng)1500個請求,每45秒鐘會新分配大約2G的內(nèi)存。G1會每45秒鐘進行一次年輕代回收,每31 個小時整個堆的使用率會達到45%,會開始老年代并發(fā)標(biāo)記過程,標(biāo)記完成后開始四到五次的混合回收。

8.10 記憶集與寫屏障

  • 一個對象被不同區(qū)域引用的問題(分代引用問題)
  • 一個Region不可能是孤立的,一個Region中的對象可能被其他任意Region中對象引用,判斷對象存活時,是否需要掃描整個Java堆才能保證準(zhǔn)確?
  • 在其他的分代收集器,也存在這樣的問題( 而G1更突出)
  • 回收新生代也不得不同時掃描老年代?
  • 這樣的話會降低MinorGC的效率;
  • 解決方法:
    • 無論G1還是其他分代收集器,JVM都是使用RememberedSet來避免全局掃描:

    • 每個Region都有 一個對應(yīng)的Remembered Set;

    • 每次Reference類型數(shù)據(jù)寫操作時,都會產(chǎn)生一個Write Barrier暫時中斷操作; .

    • 然后檢查將要寫入的引用指向的對象是否和該Reference類型數(shù)據(jù)在不同的Region (其他收集器:檢查老年代對象是否引用了新生代對象) ;

    • 如果不同,通過CardTable把相關(guān)引用信息記錄到引用指向?qū)ο蟮乃赗egion對應(yīng)的Remembered Set中;

    • 當(dāng)進行垃圾收集時,在GC根節(jié)點的枚舉范圍加入Remembered Set;就可以保證不進行全局掃描,也不會有遺漏。

8.11 G1回收過程詳解

1 年輕代GC

  • JVM啟動時,G1 先準(zhǔn)備好Eden區(qū),程序在運行過程中不斷創(chuàng)建對象到Eden區(qū),當(dāng)Eden空間耗盡時,G1會啟動一次年輕代垃圾回收過程。
  • 年輕代垃圾回收只會回收Eden區(qū)和Survivor區(qū)。
  • YGC時,首先G1停止應(yīng)用程序的執(zhí)行(Stop-The-World),G1創(chuàng)建回收集(Collection Set),回收集是指需要被回收的內(nèi)存分段的集合,年輕代回收過程的回收集包含年輕代Eden區(qū)和Survivor區(qū)所有的內(nèi)存分段。


  • 然后開始如下回收過程:
    • 第一階段,掃描根。
      根是指static變量指向的對象,正在執(zhí)行的方法調(diào)用鏈條上的局部變量等。根引用連同RSet(記憶集)記錄的外部引用作為掃描存活對象的入口。
    • 第二階段,更新RSet.
      處理dirty card queue中的card,更新RSet。 此階段完成后,RSet可 以準(zhǔn)確的反映老年代對所在的內(nèi)存分段中對象的引用。
      • dirty card queue: 對于應(yīng)用程序的引用賦值語句object.field=object,JVM會在之前和之后執(zhí)行特殊的操作以在dirty card queue中入隊一個保存了對象引用信息的card。在年輕代回收的時候, G1會對Dirty Card Queue中所有的card進行處理,以更新RSet,保證RSet實時準(zhǔn)確的反映引用關(guān)系。 那為什么不在引用賦值語句處直接更新RSet呢?這是為了性能的需要,RSet的處理需要線程同步,開銷會很大,使用隊列性能會好很多。
    • 第三階段,處理RSet
      識別被老年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認為是存活的對象。
    • 第四階段,復(fù)制對象。
      此階段,對象樹被遍歷,Eden區(qū)內(nèi)存段中存活的對象會被復(fù)制到Survivor區(qū)中空的內(nèi)存分段,Survivor區(qū)內(nèi)存段中存活的對象如果年齡未達閾值,年齡會加1,達到閥值會被會被復(fù)制到Old區(qū)中空的內(nèi)存分段。如果Survivor空間不夠,Eden空間的 部分?jǐn)?shù)據(jù)會直接晉升到老年代空間。
    • 第五階段,處理引用。
      處理Soft,Weak, Phantom, Final, JNI Weak等引用。最終Eden空間的數(shù)據(jù)為空,GC停止工作,而目標(biāo)內(nèi)存中的對象都是連續(xù)存儲的,沒有碎片,所以復(fù)制過程可以達到內(nèi)存整理的效果,減少碎片。

2 并發(fā)標(biāo)記過程

  • 初始標(biāo)記階段:標(biāo)記從根節(jié)點直接可達的對象。這個階段是STW的,并且會觸發(fā)一次年輕代GC。
  • 根區(qū)域掃描(Root Region Scanning) : G1 GC掃描Survivor區(qū)直接可達的老年代區(qū)域?qū)ο?,并?biāo)記被引用的對象。這一過程必須在young GC之前完成。
  • 并發(fā)標(biāo)記(Concurrent Marking): 在整個堆中進行并發(fā)標(biāo)記(和應(yīng)用程序并發(fā)執(zhí)行),此過程可能被young GC中斷。在并發(fā)標(biāo)記階段,若發(fā)現(xiàn)區(qū)域?qū)ο笾械乃袑ο蠖际抢?,那這個區(qū)域會被立即回收。同時,并發(fā)標(biāo)記過程中,會計算每個區(qū)域的對象活性(區(qū)域中存活對象的比例)。
  • 再次標(biāo)記(Remark): 由 于應(yīng)用程序持續(xù)進行,需要修正上一次的標(biāo)記結(jié)果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
  • 獨占清理(cleanup,STW):計算各個區(qū)域的存活對象和GC回收比例,并進行排序,識別可以混合回收的區(qū)域。為下階段做鋪墊。是STW的。
    • 這個階段并不會實際上去做垃圾的收集
  • 并發(fā)清理階段:識別并清理完全空閑的區(qū)域。

3 混合回收

當(dāng)越來越多的對象晉升到老年代oldregion時,為了避免堆內(nèi)存被耗盡,虛擬機會觸發(fā)一個混合的垃圾收集器,即Mixed GC, 該算法并不是一個OldGC,除了回收整個Young Region,還會回收一部分的0ldRegion。這里需要注意:是一部分老年代, 而不是全部老年代??梢赃x擇哪些oldRegion進行收集,從而可以對垃圾回收的耗時時間進行控制。也要注意的是Mixed GC并不是Full GC。

  • 并發(fā)標(biāo)記結(jié)束以后,老年代中百分百為垃圾的內(nèi)存分段被回收了,部分為垃圾的內(nèi)存分段被計算了出來。默認情況下,這些老年代的內(nèi)存分段會分8次(可以通過一XX: G1MixedGCCountTarget設(shè)置)被回收。
  • 混合回收的回收集(Collection Set) 包括八分之一的老年代內(nèi)存分段,Eden區(qū)內(nèi)存分段,Survivor區(qū)內(nèi)存分段。混合回收的算法和年輕代回收的算法完全一樣,只是回收集多了老年代的內(nèi)存分段。具體過程請參考上面的年輕代回收過程。
  • 由于老年代中的內(nèi)存分段默認分8次回收,G1會優(yōu)先回收垃圾多的內(nèi)存分段。垃圾占內(nèi)存分段比例越高的,越會被先回收。并且有一個閾值會決定內(nèi)存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默認為65%,意思是垃圾占內(nèi)存分段比例要達到65%才會被回收。如果垃圾占比太低,意味著存活的對象占比高,在復(fù)制的時候會花費更多的時間。
  • 混合回收并不一定要進行8次。有一個閾值-XX: G1HeapWastePercent,默認值為10%,意思是允許整個堆內(nèi)存中有10%的空間被浪費,意味著如果發(fā)現(xiàn)可以回收的垃圾占堆內(nèi)存的比例低于10%,則不再進行混合回收。因為GC會花費很多的時間但是回收到的內(nèi)存卻很少。

4 Full GC

??G1的初衷就是要避免Full GC的出現(xiàn)。但是如果上述方式不能正常工作,G1會停止應(yīng)用程序的執(zhí)行(Stop-The-World),使用單線程的內(nèi)存回收算法進行垃圾回收,性能會非常差,應(yīng)用程序停頓時間會很長。
??要避免Full GC的發(fā)生,一旦發(fā)生需要進行調(diào)整。什么時候會發(fā)生Full GC呢?比如堆內(nèi)存太小,當(dāng)G1在復(fù)制存活對象的時候沒有空的內(nèi)存分段可用,則會回退到full gc, 這種情況可以通過增大內(nèi)存解決。

導(dǎo)致G1Full GC的原因可能有兩個:

  • 1.Evacuation的時候沒有足夠的to一 space來存放晉升的對象;
  • 2.并發(fā)處理過程完成之前空間耗盡。

補充

??從Oracle官方透露出來的信息可獲知,回收階段(Evacuation)其實.本也有想過設(shè)計成與用戶程序一起并發(fā)執(zhí)行,但這件事情做起來比較復(fù)雜,考慮到G1只是回收一部分Region, 停頓時間是用戶可控制的,所以并不迫切去實現(xiàn),而選擇把這個特性放到了G1之后出現(xiàn)的低延遲垃圾收集器(即ZGC)中。另外,還考慮到G1不是僅僅面向低延遲,停頓用戶線程能夠最大幅度提高垃圾收集效率,為了保證吞吐量所以才選擇了完全暫停用戶線程的實現(xiàn)方案。

8.12 優(yōu)化建議

  • 年輕代大小
    • 避免使用-Xmn或-XX:NewRatio等相關(guān)選項顯式設(shè)置年輕代大小。固定年輕代的大小會覆蓋暫停時間目標(biāo)
  • 暫停時間目標(biāo)不要太過嚴(yán)苛
    • G1 GC的吞吐量目標(biāo)是90%的應(yīng)用程序時間和10%的垃圾回收時間
    • 評估G1 GC的吞吐量時,暫停時間目標(biāo)不要太嚴(yán)苛。目標(biāo)太過嚴(yán)苛表示你愿意承受更多的垃圾回收開銷,而這些會直接影響到吞吐量。

9 垃圾回收器總結(jié)


截止JDK 1.8,一共有7款不同的垃圾收集器。每一款不同的垃圾收集器都有不同的特點,在具體使用的時候,需要根據(jù)具體的情況選用不同的垃圾收集器。


不同廠商、不同版本的虛擬機實現(xiàn)差別很大。HotSpot 虛擬機在JDK7/8后所有收集器及組合(連線),如下圖:
[圖片上傳中...(image-4da874-1596784089014-19)]

GC發(fā)展階段: Serial => Parallel (并行) => CMS (并發(fā)) => G1 => ZGC

怎么選擇垃圾回收器

  • Java垃圾收集器的配置對于JVM優(yōu)化來說是一個很重要的選擇,選擇合適的垃圾收集器可以讓JVM的性能有一個很大的提升。

  • 怎么選擇垃圾收集器?

    • 1.優(yōu)先調(diào)整堆的大小讓JVM自適應(yīng)完成。
    • 2.如果內(nèi)存小于100M,使用串行收集器
    • 3.如果是單核、單機程序,并且沒有停頓時間的要求,串行收集器
    • 4.如果是多CPU、需要高吞吐量、允許停頓時間超過1秒,選擇并行或者JVM自己選擇
    • 5.如果是多CPU、追求低停頓時間,需快速響應(yīng)(比如延遲不能超過1秒,如互聯(lián)網(wǎng)應(yīng)用),使用并發(fā)收集器
  • 最后需要明確一一個觀點:

    • 1.沒有最好的收集器,更沒有萬能的收集;
    • 2.調(diào)優(yōu)永遠是針對特定場景、特定需求,不存在一勞永逸的收集器

10 GC日志分析


通過閱讀GC日志,我們可以了解Java虛擬機內(nèi)存分配與回收策略。內(nèi)存分配與垃圾回收的參數(shù)列表

  • -XX:+PrintGC 輸出GC日志。類似: -verbose:gc
  • -XX: +PrintGCDetails 輸出GC的詳細日志
  • -XX: +PrintGCTimeStamps 輸出GC的時間戳(以基準(zhǔn)時間的形式)
  • -XX: +PrintGCDateStamps輸出GC的時間戳(以日期的形式,如2013-05-04T21 : 53:59.234+0800 )
  • -XX: +PrintHeapAtGC 在進行GC的前后打印出堆的信息
  • -Xloggc: . . /logs/gc. log日志文件的輸出路徑
+PrintGC
  • 打開GC日志:-verbose:gc / -XX:+PrintGC
  • 這個只會顯示總的GC堆的變化, 如下:
[GC (Allocation Failure) 80832K一>19298K(227840K),0.0084018 secs]
[GC (Metadata GC Threshold) 109499K一>21465K (228352K),0.0184066 secs]
[Full GC (Metadata GC Threshold) 21 465K一>16716K (201728K),0.0619261 secs ]
  • 參數(shù)解析:
GC、Full GC: GC的類型,GC只在新生代上進行,F(xiàn)ull GC包括永生代,新生代, 老年代。
Allocation Failure: GC發(fā)生的原因。
80832K一> 19298K:堆在GC前的大小和GC后的大小。
228840k:現(xiàn)在的堆大小。
0.0084018 secs: GC持續(xù)的時間。
PrintGCDetails
  • 打開GC日志: -XX:+PrintGCDetaiis

  • 輸入信息如下:

[GC (Allocation Failure) [ PSYoungGen: 70640K一> 10116K(141312K) ] 80541K一>20017K (227328K),0.0172573 secs] [Times: user=0.03 sys=0.00, real=0.02 secs ]
[GC (Metadata GC Threshold) [PSYoungGen:98859K一>8154K(142336K) ] 108760K一>21261K (228352K),
0.0151573 secs] [Times: user=0.00 sys=0.01, real=0.02 secs]
[Full GC (Metadata GC Threshold) [PSYoungGen: 8154K一>0K(142336K) ] [ParOldGen: 13107K一>16809K(62464K) ] 21261K一>16809K (204800K),[Metaspace: 20599K一>20599K (1067008K) ],0.0639732 secs]
[Times: user=0.14 sys=0.00, real=0.06 secs]
  • 參數(shù)解析:
GC,F(xiàn)ull FC:同樣是GC的類型
Allocation Failure: GC原因
PSYoungGen:使用了Parallel Scavenge并行垃圾收集器的新生代GC前后大小的變化
ParOldGen:使用了Parallel Old并行垃圾收集器的老年代GC前后大小的變化
Metaspace: 元數(shù)據(jù)區(qū)GC前后大小的變化,JDK1.8中引入了 元數(shù)據(jù)區(qū)以替代永久代
xxx secs : 指Gc花費的時間
Times: user: 指的是垃圾收集器花費的所有CPU時間,sys: 花費在等待系統(tǒng)調(diào)用或系統(tǒng)事件的時間, real :GC從開始到結(jié)束的時間,包括其他進程占用時間片的實際時間。

PrintGCTimeStamps

  • 打開GC日志: -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
  • 輸入信息如下:
2019一09一24T22:15:24.518+0800:3.287: [GC(Allocation Failure) [ PSYoungGen: 1361 62K一>5113K(136192K) ] 141425K一>17632K (222208K) ,0.0248249 secs] [Times: user=0.05sys=0.00, real=0.03 secs ]
2019一09一24T22:15:25.559+0800:4.329: [ GC(Metadata GC Threshold)[PSYoungGen:97578K一>10068K(274944K) ] 110096K一>22658K (360960K),0.0094071 secs]
[Times: user=0\. 00sys=0.00, real=0\. 01 secs]
2019一09一24T22:15:25.569+0800:4.338: [Full GC (Metadata GC Threshold)[ PSYoungGen:10068K一>0K(274944K) ] [ ParoldGen: 12590K一>13564K (56320K) ] 22658K一>13564K (331264K) ,
[Metaspace: 20590K一>20590K(1067008K)], 0\. 0494875 secs]
[Times: user=0.17 sys=0\. 02,real=0.05 secs ]     

說明:帶上了日期和時間

補充說明
  • "[GC"和"[Full GC"說明了這次垃圾收集的停頓類型,如果有"Full"則說明GC發(fā)生了"StopThe World"
  • 使用Serial收集器在新生代的名字是Default New Generation, 因此顯示的是" [DefNew"
  • 使用ParNew收集器在新生代的名字會變成" [ParNew",意思是"Parallel New Generation"
  • 使用Parallel Scavenge收 集器在新生代的名字是"[PSYoungGen"
  • 老年代的收集和新生代道理一樣,名字也是收集器決定的
  • 使用G1收集器的話,會顯示為"garbage-first heap"
  • Allocation Failure 表明本次引起GC的原因是因為在年輕代中沒有足夠的空間能夠存儲新的數(shù)據(jù)了。
  • [PSYoungGen: 5986K一>696K(8704K)] 5986K一> 704K (9216K)
    • 中括號內(nèi): GC回收前年輕代占用大小,回收后占用大小,( 年輕代總大小)
    • 括號外: GC回收前年輕代和老年代占用大小,回收后占用大小,( 年輕代和老年代總大?。?/li>
  • user代表用戶態(tài)回收耗時,sys 內(nèi)核態(tài)回收耗時, rea實際耗時。由于多核的原因,時間總和可能會超過real時間
堆空間日志分析

Minor GC
Minor GC日志分析

Full GC
Full GC日志分析
/**
 * 在jdk7 和 jdk8中分別執(zhí)行
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 */
public class GCLogTest1 {
    private static final int _1MB = 1024 * 1024;

    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] agrs) {
        testAllocation();
    }
}

GC前

GC后
日志分析工具使用

可以用一些工具去分析這些gc日志。
常用的日志分析工具有: GCViewer、GCEasy、GCHisto、GCLogViewer 、Hpjmeter、garbagecat等。

11 垃圾回收器的新發(fā)展

??GC仍然處于飛速發(fā)展之中,目前的默認選項G1 GC在不斷的進行改進,很多我們原來認為的缺點,例如串行的Full GC、Card Table掃描的低效等,都已經(jīng)被大幅改進,例如,JDK 10以后,F(xiàn)u1l GC已經(jīng)是并行運行,在很多場景下,其表現(xiàn)還略優(yōu)于Parallel GC的并行Full GC實現(xiàn)。
??即使是Serial GC,雖然比較古老,但是簡單的設(shè)計和實現(xiàn)未必就是過時的,它本身的開銷,不管是GC相關(guān)數(shù)據(jù)結(jié)構(gòu)的開銷,還是線程的開銷,都是非常小的,所以隨著云計算的興起,在Serverless等新的應(yīng)用場景下,Serial GC找到了新的舞臺。
??比較不幸的是CMS GC, 因為其算法的理論缺陷等原因,雖然現(xiàn)在還有非常大的用戶群體,但在JDK9中已經(jīng)被標(biāo)記為廢棄,并在JDK14版本中移除。

JDK11 新特性

  • JEP318 : Epsilon: A No-Op Garbage Collector (Epsilon 垃圾回收器,"No-Op (無操作) "回收器) http: / /openidk.java.net/ieps/318
  • JEP333: ZGC: A Scalable Low一 Latency ;Garbage Collector (Experimental) ( ZGC:可伸縮的低延遲垃圾回收器,處于試驗性階段)

Open JDK12 的Shenandoah GC:低停頓時間的GC (實驗性)

  • 現(xiàn)在G1回收器已成為默認回收器好幾年了。

  • 我們還看到了引入了兩個新的收集器: ZGC ( JDK11出現(xiàn))和Shenandoah(Open JDK12) 。

    • 主打特點:低停頓時間
  • Shenandoah,無疑是眾多GC中最孤獨的一個。是第一款不由Oracle公司團隊領(lǐng)導(dǎo)開發(fā)的HotSpot垃圾收集器。不可避免的受到官方的排擠。比如號稱OpenJDK和OracleJDK沒有區(qū)別的Oracle公司仍拒絕在OracleJDK12中支持Shenandoah。

  • Shenandoah垃圾回收器最初由RedHat進行的一項垃 圾收集器研究項目PauselessGC的實現(xiàn),旨在針對JVM上的內(nèi)存回收實現(xiàn)低停頓的需求。在2014年貢獻給OpenJDK。

  • Red Hat研發(fā)Shenandoah團隊對外宣稱,Shenandoah垃圾回收器的暫停時間與堆大小無關(guān),這意味著無論將堆設(shè)置為200MB還是200GB,99.9%的目標(biāo)都可以把垃圾收集的停頓時間限制在十毫秒以內(nèi)。不過實際使用性能將取決于實際工作堆的大小和工作負載。


  • 這是RedHat在2016年發(fā)表的論文數(shù)據(jù),測試內(nèi)容是使用Es對200GB的維基百科數(shù)據(jù)進行索引。從結(jié)果看:

    • 停頓時間比其他幾款收集器確實有了質(zhì)的飛躍,但也未實現(xiàn)最大停頓時間控制在十毫秒以內(nèi)的目標(biāo)。
    • 而吞吐量方面出現(xiàn)了明顯的下降,總運行時間是所有測試收集器里最長的。
  • Shenandoah GC的弱項:高運行負擔(dān)下的吞吐量下降。

  • Shenandoah GC的強項:低延遲時間。

革命性的ZGC

??ZGC官網(wǎng)鏈接與Shenandoah目標(biāo)高度相似,在盡可能對吞吐量影響不大的前提下,實現(xiàn)在任意堆內(nèi)存大小下都可以把垃圾收集的停頓時間限制在十毫秒以內(nèi)的低延遲。
??《深入理解Java虛擬機》一書中這樣定義ZGC: ZGC收集器是一款基于Region內(nèi)存布局的,(暫時) 不設(shè)分代的,使用了讀屏障、染色指針和內(nèi)存多重映射等技術(shù)來實現(xiàn)可并發(fā)的標(biāo)記-壓縮算法的,以低延遲為首要目標(biāo)的一款垃圾收集器。
??ZGC的工作過程可以分為4個階段:并發(fā)標(biāo)記一并發(fā)預(yù)備重分配一并發(fā)重分配一并發(fā)重映射等。
??ZGC幾乎在所有地方并發(fā)執(zhí)行的,除了初始標(biāo)記的是STW的。所以停頓時間幾乎就耗費在初始標(biāo)記上,這部分的實際時間是非常少的。

測試數(shù)據(jù)如圖:

  • 劣勢比較


  • 優(yōu)勢比較


在ZGC的強項停頓時間測試上,它毫不留情的將Parallel、G1拉開了兩個數(shù)量級的差距。無論平均停頓、958停頓、998停頓、99. 98停頓,還是最大停頓時間,ZGC 都能毫不費勁控制在10毫秒以內(nèi)。

JDK14新特性

  • JEP 364: ZGC應(yīng)用在macOS上

  • JEP 365: ZGC應(yīng)用在windows上** JDK14之前,ZGC僅Linux才支持

  • 盡管許多使用ZGC的用戶都使用類Linux的環(huán)境,但在Windows和macOS 上,人們也需要ZGC進行開發(fā)部署和測試。許多桌面應(yīng)用也可以從ZGC中受益。因此,ZGC特性被移植到了Windows和macOs.上。

  • 現(xiàn)在mac或Windows 上也能使用zGC了,示例如下: -XX:+Unloc kExperimentalVMOptions -XX: +UseZGC .

其他垃圾回收器:AliGC

AliGC是阿里巴巴JVM團隊基于G1算法,面 向大堆(LargeHeap)應(yīng)用場景。指定場景下的對比:


當(dāng)然,其他廠商也提供了各種獨具一格的GC實現(xiàn),例如比較有名的低延遲GC,Zing
https://www.infoq.com/articles/azul_gc_in_detail/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容