不可錯過的JVM深度好文!-純乾貨詳解JVM垃圾回收

JVM-垃圾回收

1. 垃圾回收相關概述

1.1 什麼是垃圾

​ 垃圾指的是在運行程序中沒有任何指針(或引用)指向的對象,這個對象就是需要回收的垃圾。 如果不及時對內存中的垃圾進行清理,那麼這些垃圾對象所佔用的內存空間一直保留到應用程序結束,被保留的空間無法被其他對象使用。可能會導致內存溢出。

​ 對於高級語言來說,如果不進行垃圾回收,因爲不斷分配內存而不進行回收,內存早晚會被消耗完。除了釋放沒有用的對象,垃圾回收也可以清除內存裏的碎片,碎片整理將所佔用的堆內存移動到堆的一端,便於JVM將整理出內存分配給新的對象。特別是大的對象,可能需要一塊連續的大的內存空間。

1.2 什麼是GC

​ **垃圾回收(Garbage Collection)**作爲一⻔實用而又􏰀要的技術,可以說拯救了無數苦於內存管理的程序員。儘管很多人認爲,GC技術走進大衆的視􏰁,多是源於Java語言的崛起,但是GC技術本身卻相當的古老。早在1960年,Lisp之父John McCarthy已經在其論文中發佈了GC算法,Lisp語言也是第 一個實現GC的語言。

​ 在 GC 最開始設計時,人們在思考 GC 時就需要完成三件事情:

  1. 哪些內存需要進行回收?
  2. 什麼時候對這些內存進行回收?
  3. 如何進行回收?

​ 垃圾回收與“java面向對象編程”一樣是java語言的特性之一;它與“ c/c++語言”最大區別是不用手動調用 free() 和 delete() 釋放內存。GC 主要是處理 JavaHeap ,也就是作用在 Java虛擬機 用於存放對象實例的內存區域,(Java堆又稱爲GC堆)。JVM能夠完成內存分配和內存回收,雖然降低了開發難度,避免了像C/C++直接操作內存的危險。但也正因爲太過於依賴JVM去完成內存管理,導致很多Java 開發者不再關心內存分配,導致很多程序低效、耗內存問題。因此開發者需要主動了解GC機制,充分利用有限的內存的程序,才能寫出更高效的程序。

在這裏插入圖片描述

垃圾回收機制仍然在不斷的迭代中,不同的場景對垃圾回收提出了新的挑戰。

1.3 STW

Stop-the-World,簡稱STW,指的是GC事件發生過程中,會產生應用程序的停頓。停頓是產生時整 個應用程序線程會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱爲STW。Stop-the-world意味着 JVM由於要執行GC而停止了應用程序(用戶線程、工作線程)的執行,並且這種情形會在任何一種GC算法中發生。當Stop-the-world發生時,除了GC所需的線程以外,所有線程都處於等待狀態直到GC任務完成。

在這裏插入圖片描述
​ STW事件和採用哪款GC無關,所有的GC都有這個事件。哪怕是G1也不能完全避免Stop-the-world 情況發生,只能說垃圾回收器越來越優秀,回收效率越來越高,儘可能縮短了暫停時間。

​ STW是JVM在後臺自動發起和自動完成的,在用戶不可⻅的情況下,把用戶正常的工作線程全部停掉。

​ 隨着應用程序越來越複雜,每次GC不能保證應用程序的正常運行。而經常造成STW的GC跟不上實際的需求,所以才需要不斷對GC進行優化。事實上,GC優化很多時候就是指減少Stop-the-world發生的時間從而使系統具有高吞吐 、低停頓的特點

1.4 並行與併發

併發(Concurrent)

​ 在操作系統中,是指一個時間段中有個幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理器上運行。

​ 併發並不是真正意義上的"同時進行",只是CPU把一個時間段劃分成幾個時間片段(時間區間),然後在這幾個時間區間之間來回切換,由於CPU處理的速度非常快,只要時間間隔處理得當,即可讓用戶感覺是多個應用程序是同時進行的。

在這裏插入圖片描述

並行**(Parallel)**

​ 當系統有一個以上CPU時,當一個CPU執行一個進程時,另外一個CPU可以執行另一個進程,兩個進程互不搶佔CPU資源,可以同時進行,我們稱之爲並行(Parallel);

​ 其實決定並行的因素不是CPU的數􏰂量,而是CPU的核心數􏰂量,比如一個CPU多個核可以並行。

​ 適合科學計算,後臺處理等弱交互場景。

在這裏插入圖片描述

二者對比:

併發,指的是多個事情,在同一時間段內同時發生了。

並行,指的是多個事情,在同一時間點上同時發生了。

併發的多個任務之間是互相搶佔資源的。

並行的多個任務之間是不互相搶佔資源的。

只有在多個CPU或者一個CPU多核的情況中,纔會發生並行。 否則,看似同時發生的事情,其實都是併發執行的。

1.5 GC分類

JVM在進行回收時,是針對不同的內存區域進行回收的,大多數的回收指的是對新生代的回收。

針對HotSpot VM的實現,它裏面的GC其實準確分類只有兩大種:

  • Partial GC:並不收集整個GC堆的模式。其中又分爲

    • 新生代的回收:(Minor GC/Young GC),只收集新生代的GC

    • 老年代的回收:(Major GC/Old GC),只收集老年代的GC。

      ​ 目前只有CMS的concurrent collection是這個模式,只收集老年代。

    • Mixed GC:收集整個young gen以及部分old gen的GC。

      ​ 只有G1有這個模式。

  • Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

​ Major GC通常是跟full GC是等價的,收集整個GC堆。但因爲HotSpot VM發展了這麼多年, 外界對各種名詞的解讀已經完全混亂了,當有人說“major GC”的時候一定要問清楚他想要指的是上面的full GC還是old GC。

約定: 新生代/新生區/年輕代 養老區/老年區/老年代/年老代 永久區/永久代

1.6 GC觸發條件

最簡單的分代式GC策略,按HotSpot VM的serial GC的實現來看,觸發條件是:

  • 年輕代(Minor GC)觸發條件:

​ 一般情況下,所有新生成的對象首先都是放在新生代的。新生代內存按照 8:1:1 的比例分爲一 個eden區和兩個survivor(from survivor,to survivor)區,大部分對象在Eden區中生成。

​ 在進行垃圾回收時,先將eden區存活對象複製到from survivor區,然後清空eden區,當這個from survivor區也滿了時,則將eden區和from survivor區存活對象複製到to survivor區,然後清空eden和這個from survivor區,此時from survivor區是空的,然後交換from survivor區和to survivor區的⻆色(即下次垃圾回收時會掃描Eden區和to survivor區),即保持from survivor區爲空,如此往復。

​ 特別地,當to survivor區也不足以存放eden區和from survivor區的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了,就會觸發一次FullGC,也就是新生代、老年代都進行回收。注意,新生代發生的GC叫做MinorGC,MinorGC發生頻率比較高,不一定等 Eden區滿了才觸發。

​ Minor GC觸發比較頻繁,一般回收速度也是比較快的。Minor GC會引發STW,暫停用戶線程,等待垃圾回收完畢後,用戶線程纔會恢復。

在這裏插入圖片描述

  • 老年代(Major GC)觸發條件:

(1) 由Eden區、from survivor區向to survivor區複製時,對象大小大於to survivor可用內存,則把該對象轉存到老年代,會先嚐試觸發Minor GC,如果之後空間還是不足,則會觸發Major GC。

(2) 如果Major GC後還是不足,就會OOM。

(3) 發生Major GC,通常伴隨Minor GC,但這並不是絕對的,Parallel Scavage這種收集器就有直接進行Major GC的策略過程。

說明:Major GC的速度一般會比Minor GC的速度慢10倍以上。

  • FULL GC觸發條件:

(1)System.gc()方法的調用

​ 此方法的調用是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。建議能不使用此方法就別使用,讓虛擬機自己去管理它的內存,可通過-XX:+DisableExplicitGC來禁止RMI(Java遠程方法調用)調用 System.gc。

(2)老年代空間不足

​ 老年代空間只有在新生代對象轉入及創建爲大對象、大數組時纔會出現不足的現象,當執行Full GC後空間仍然不足,則拋出如下錯誤: java.lang.OutOfMemoryError: Java heap space,爲避免以上兩種狀況引起的FullGC,調優時應盡􏰂做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。

(3)方法區空間不足

​ JVM規範中運行時數據區域中的方法區,在HotSpot虛擬機中又被習慣稱爲永生代或者永生區,Permanet Generation中存放的爲一些class的信息、常􏰂量、靜態變􏰂等數據,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用 CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會拋出如下錯誤信 息:

​ java.lang.OutOfMemoryError: PermGen space

​ 爲避免Perm Gen佔滿造成Full GC現象,可採用的方法爲增大Perm Gen空間或轉爲使用CMS GC。

(4)通過Minor GC後進入老年代的平均大小大於老年代的可用內存

如果發現統計數據說之前Minor GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發Minor GC而是轉爲觸發full GC

(5)由Eden區、from survivor區向to survivor區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

2. 檢測垃圾

​ 在堆裏放着幾乎所有的java對象實例,在GC執行垃圾回收之前,首先需要區分出內存中哪些是存活對象,哪些是已經死亡的對象,只有被標記爲已經死亡的對象,GC纔會在執行垃圾回收,釋放掉其所佔用的內存空間,因此這個過程我們可以稱爲垃圾標記階段

​ 那麼在JVM中究竟是如何標記一個對象是死亡的呢?簡單地說,當一個對象已經不再被任何的存活對象繼續引用時,就可以判斷爲已經死亡。

​ 判斷對象存活一般有兩種方式:引用計數算法可達性分析算法

2.1 引用計數算法(Reference Counting)

​ 引用計數算法:通過判斷對象的引用數􏰂來決定對象是否可以被回收。

​ 引用計數算法是垃圾收集器中的早期策略。在這種方法中,堆中的每個對象實例都有一個引用計數。當一個對象被創建時,且將該對象實例分配給一個引用變􏰂量,該對象實例的引用計數設置爲 1。當 任何其它變􏰂被賦值爲這個對象的引用時,對象實例的引用計數加 1(a = b,則b引用的對象實例的計數器加1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數減1。特別地,當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器均減1。任何引用計數爲0的對象實例可以被當作垃圾收集。

​ 引用計數收集器可以很快的執行,並且交織在程序運行中,對需要不被⻓時間打斷的實時環境比較 利,但其很難解決對象之間相互循環引用的問題。如下面示例所示,對象objA和objB之間的引用計數永遠不可能爲0,那麼這兩個對象就永遠不能被回收。

在這裏插入圖片描述

/**
 * -Xms10m -Xmx10m -XX:+PrintGCDetails * 證明java使用的不是引用計數器算法
 */
public class ReferenceCountGC {
    public Object instance = null;
    private byte[] bigObject = new byte[1024*1024];
    public static void main(String[] args){
        ReferenceCountGC objA = new ReferenceCountGC ();
        ReferenceCountGC objB = new ReferenceCountGC ();
// 對象之間相互循環引用,對象objA和objB之間的引用計數永遠不可能爲 0 objB.instance = objA;
        objA.instance = objB;
        objA = null;
        objB = null;
        System.gc(); //通過註釋,打開或關閉垃圾回收的執行
        }
    }

​ 上述代碼最後面兩句將objA和objB賦值爲null,也就是說objA和objB指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數器都不爲0,那麼垃圾收集器就永遠不會回收它們。

優點:

  • 實現簡單,垃圾對象便於標識;
  • 判定效率高,回收沒有延遲性。

缺點:

  • 它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷。
  • 每次賦值都需要更新計數器,伴隨這加法和減法操作,這就增加了時間開銷。
  • 致命缺陷,即無法處理循環引用的情況。導致在java的垃圾回收器中沒有使用這類算法。

擴展知識點

​ java並沒有選擇引用計數,是因爲其存在一個基本的難題,也就是很難處理循環引用關係。引用計數算法,是很多語言的資源回收選擇,例如python,它更是同時支持引用計數和垃圾收集機制。 Python如何解決循環引用?

  • ​ 手動解除:很好理解,就是在合適的時機,解除引用關係。
  • ​ 使用弱引用weakref,weakref是Ptyhon提供的標準庫,旨在解決循環引用。

2.2 可達性分析算法(Rearchability Analysis)

2.2.1 概述

​ 相對於引用計數算法,這裏的可達性分析是java、c# 選擇的。這種類型的垃圾收集通常也叫追蹤性垃圾收集****(Tracing Garbage Collection)

​ 可達性分析算法是通過判斷對象的引用鏈是否可達來決定對象是否可以被回收。

​ 可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關係看作一張圖,通過一系列的名爲 “GC Roots” 的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈 (Reference Chain)。當一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達)時,則證明此對象是不可用的,如下圖所示。

在這裏插入圖片描述

在java中,可作爲 GC Root 的對象包括以下幾種:

  1. 虛擬機棧(棧幀中的局部變􏰂表)中引用的對象;

​ 比如:各個線程被調用的方法中使用的參數、局部變􏰂量等。

  1. 方法區中類靜態屬性引用的對象;

​ 比如:java類的引用類型靜態變量􏰂

3)方法區中常􏰂引用的對象;

​ 比如:字符串常􏰂池(String Table)裏的引用

4)本地方法棧中Native方法引用的對象;

5)所有被同步鎖synchronized持有的對象;

  1. java虛擬機內部的引用;

​ 比如:基本數據類型對應的Class對象,一些異常對象(如:NullPointerException、 OutOfMemoryError),系統類加載器

7)反映java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

​ 由於Root採用棧方式存放變􏰂和指針,所以如果一個指針,它保存了堆內存裏面的對象地址,但是自己又不存放在堆內存裏面,那它就是一個Root。

2.2.2 代碼演示

下面以一段代碼來簡單說明一下

 
class RearchabilityTest {
	private static A a = new A(); // 靜態變量􏰂
	public static final String CONTANT = "I am a string"; // 常量􏰂 
	public static void main(String[] args) {
	A innerA = new A(); // 局部變􏰂量 
	}
}
class A { 
}

在這裏插入圖片描述

​ 首先,類加載器加載RearchabilityTest類,會初始化靜態變量􏰂a,將常􏰂量引用指向常量􏰂池中的字符串,完成RearchabilityTest類的加載; 然後main方法執行,main方法會入虛擬機方法棧,執行main方法會在堆中創建A的對象,並賦值給局部變􏰂量innerA。

​ 此時GC Roots狀態如下:

在這裏插入圖片描述

​ 當main方法執行完出棧後,變爲:

在這裏插入圖片描述

​ 第三個對象已經沒有引用鏈可達GC Root。此時,第三個對象被第一次標記。

2.2.4 使用MAT查看GC Roots

MAT是一個強大的內存分析工具,可以快捷、有效地幫助我們找到內存泄露,減少內存消耗分析工具。

MAT是Memory Analyzer tool的縮寫,是一種快速,功能豐富的Java堆分析工具,能幫助你查找內存泄漏和減少內存消耗。很多情況下,我們需要處理測試提供的hprof文件,分析內存相關問題,那麼 MAT也絕對是不二之選。

MAT安裝有兩種方式,一種是以eclipse插件方式安裝,一種是獨立安裝。在MAT的官方文檔中有相應的安裝文件下載,下載地址爲:https://www.eclipse.org/mat/downloads.php

在這裏插入圖片描述


import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
public class GCRootsTest {
    public static void main(String[] args) {
    List<Object> numList = new ArrayList<>();
    Date birth = new Date();
    for (int i = 0; i < 100; i++)
    {
        numList.add(String.valueOf(i));
        try
        {
            Thread.sleep(10);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    System.out.println("數據添加完畢,請下一步操作:");
    new Scanner(System.in).next();
    numList = null;
    birth = null;
    System.out.println("numList、birth已置空,請下一步操作:");
    new Scanner(System.in).next();
    System.out.println("結束");
    }
}

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

3. 垃圾收集算法

​ 當成功區分出內存中存活對象和死亡對象後,GC接下來的任務就是執行垃圾回收,釋放掉無用對象所佔用的內存空間,以便有足夠的可用內存空間爲新對象分配內存。

目前在JVM中比較常⻅的三種垃圾回收算法是標記-清除算法(Mark-Sweep)、複製算法(Copying)、標記-壓縮算法(Mark-Compact)

3.1 標記清除算法

​ 標記-清除算法(Mark-Sweep)是一種非常基礎和常⻅的垃圾收集算法,該算法被J.McCarthy等人在 1960年提出並應用於Lisp語言。

​ 清除算法分爲標記和清除兩個階段。該算法首先從根集合進行掃描,對存活的對象對象標記,標記完畢後,再掃描整個空間中未被標記的對象並進行回收,如下圖所示。

在這裏插入圖片描述

  • ​ 標記:GC從引用根節點開始遍歷,標記所有被引用的對象。一般是在對象的Header中記錄。
  • ​ 清除:GC對堆內存從頭到尾進行線性的遍歷,如果發現某個對象再其Header中沒有標記爲可達對象,則將其回收。

標記-清除算法的主要不足有:

  • ​ 效率問題:標記和清除兩個過程的效率都不高;
  • ​ 在進行GC的時候,需停止整個應用程序,導致用戶體驗性差;
  • ​ 空間問題:標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,因此標記清除之後會產生大􏰂不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

在這裏插入圖片描述
在這裏插入圖片描述

3.2 複製算法

​ 爲了解決標記-清除算法在垃圾收集效率方面的缺陷, M. L. Minsky 於1963 年發表了著名的論 文“一種使用雙存儲區的 Lisp 語言垃圾收集器( A LISP Garbage Collector Algorithm Using Serial Secondary Storage )”。 M. L. Minsky 在該論文中描述的算法被人們稱爲複製算法(Copying),它也被 M. L. Minsky 本人成功地引入到了 Lisp 語言的一個實現版本中。

​ 複製算法別出心裁地將堆空間一分爲二,並使用簡單的複製操作來完成垃圾收集工作,這個思路相當有趣。

​ 複製算法將可用內存按容􏰂劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完 了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這種算法適用於對象存活率低的場景,比如新生代。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。該算法示意圖如下所示:

在這裏插入圖片描述

應用場景:

​ 事實上,現在商用的虛擬機都採用這種算法來回收新生代。因爲研究發現,新生代中的對象每次回收都基本上只有**10%**左右的對象存活,所以需要複製的對象很少,效率還不錯。不適合存活􏰂對象比較多的場景。

優點:

  • 沒有標記和清除過程,實現簡單,運行高效
  • 複製過去以後保證空間的連續性,不會出現"碎片" 問題。

缺點:

  • 此算法的缺點也是很明顯,就是需要兩倍的內存空間
  • 如果對象的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要將所有對象都複製一遍,並將所有引用地址􏰀置一遍。複製這一工作所花費的時間,在對象存活率達到一定程度時, 將會變的不可忽視。

3.3 標記整理算法

​ 複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。

​ 標記-整理算法或標記-壓縮算法(Mark-Compact)是標記-清除算法和複製算法的有機結合。把標記-清除算法在內存佔用上的優點和複製算法在執行效率上的特⻓綜合起來,這是所有人都希望看到的結果。不過,兩種垃圾收集算法的整合並不像 1 加 1 等於 2 那樣簡單,我們必須引入一些全新的思路。 1970 年前後, G. L. Steele , C. J. Cheney 和 D. S. Wise 等研究者陸續找到了正確的方向,標記-整理算法的輪廓也逐漸清晰了起來。

​ 標記-整理算法的標記過程類似標記清除算法,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存,類似於磁盤整理的過程,該垃圾回收算法適用於對象存活率高的場景(老年代),其作用原理如下圖所示。

在這裏插入圖片描述

​ 標記-整理算法與標記-清除算法最顯著的區別是:標記-清除算法不進行對象的移動,並且僅對不存活的對象進行處理;而標記整理算法會將所有的存活對象移動到一端,並對不存活對象進行處理,因此 其不會產生內存碎片。標記-整理算法的作用示意圖如下:

在這裏插入圖片描述

​ 標記-整理算法的最終效果等同於標記-清除算法執行完成後,再進行一次內存碎片整理,因此,也可以把它稱爲標記-清除-壓縮(Mark-Sweep-Compact)算法。

​ 二者的本質差異在於標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收後的存活對象是一項優缺點並存的⻛險決策。

​ 可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。 如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。

優點:

  • 消除了標記-清除算法當中,內存區域分散的缺點,我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可。
  • 消除了複製算法當中,內存兩倍的高額代價。

缺點:

  • 從效率上來說,標記-整理算法要低於複製算法。
  • 移動對象的同時,如果對象被其他對象引用,則還需要調整引用的地址。
  • 移動過程中,需要全程暫停用戶應用程序。即:STW

對比三種算法

在這裏插入圖片描述

​ 效率上來說,複製算法是最快的,但是卻浪費了太多的內存。

​ 而爲了兼顧上面提到的三個指標,標記**-**整理算法相對來說更平滑一些,但是效率上不盡如人意,它比複製算法多了一個標記的階段,比標記-清除多了一個整理內存的階段。

3.4 分代收集算法

​ 前面所有這些算法中,並沒有一種算法可以完全替代其他算法,它們都具有自己獨特的優勢和特點。分代收集算法應運而生。

​ 分代收集算法(Generational Collecting),是基於這樣的一個事實:不同的對象生命週期是不一樣 的。因此不同生命週期的對象可以採取不同的收集方式,以便提高回收效率。一般是把java堆分成新生 代和老年代,這樣就可以根據各個年代的特點使用不同的回收算法,以提高垃圾回收的效率。

1、新生代(Young Generation)

新生代特點:區域相對老年代較小,對象生命週期短、存活率低,回收頻繁。

這種情況適合複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對象大小有關, 因此很適用於年輕代的回收。而複製算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。

2、老年代(Old Generation)

老年代特點:區域較大,對象生命週期⻓、存貨效率高,回收不及年輕代頻繁

這種情況存在大􏰂存活率高的對象,複製算法明顯變得不合適。一般是由標記-清除或者是標記-清除與標記-整理的混合實現。

  • 標記(Mark)階段的開銷與存活對象的數􏰂成正比;
  • 清除(Sweep)階段的開銷與所管理區域的大小成正比相關;
  • 壓縮(Compact)階段的開銷與存活對象的數據成正比。

老年代存放的都是一些生命週期較⻓的對象,就像上面所敘述的那樣,在新生代中經歷了N次垃圾回收後仍然存活的對象就會被放到老年代中。此外,老年代的內存也比新生代大很多(大概比例是1:2), 當老年代滿時會觸發Major GC(Full GC),老年代對象存活時間比較⻓,因此FullGC發生的頻率比較低。

3、 永久代(Permanent Generation)

永久代主要用於存放靜態文件,如Java類、方法等。永久代對垃圾回收沒有顯著影響,但是有些應 用可能動態生成或者調用一些class,例如使用反射、動態代理、CGLib等bytecode框架時,在這種時候需要設置一個比較大的永久代空間來存放這些運行過程中新增的類。

分代的思想被現有的虛擬機廣泛使用。幾乎所有的垃圾回收器都區分新生代和老年代。

3.5 增量式垃圾回收

增􏰂量式垃圾回收並不是一個新的回收算法, 而是結合之前算法的一種新的思路。

之前說的各種垃圾回收, 都需要暫停程序, 執行GC, 這就導致在GC執行期間, 程序得不到執行. 因此出現了增量􏰂式垃圾回收, 它並不會等GC執行完, 纔將控制權交回程序, 而是一步一步執行, 跑一點, 再跑一 點, 逐步完成垃圾回收**,** 在程序運行中穿插進行。極大地降低了GC的最大暫停時間。

總體來說,增􏰂量式垃圾回收算法的基礎仍是傳統的標記-清除和複製算法。增􏰂量式垃圾回收通過對線程間衝突的妥善處理,允許垃圾收集線程以分階段的方式完成標記、清理或複製工作。

缺點:

使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因爲線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐􏰂的 下降。

3.6 分區算法

一般來說,在相同條件下,堆空間越大,一次GC時所需要的時間也就越⻓,有關GC產生的停頓也越⻓。爲了更好的控制GC產生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標的停頓時 間,每次合理回收若干個小區件,而不是整個堆空間,從而減少一次GC所產生的停頓。

分代算法將按照對象的生命週期⻓短劃分成兩個部分,分區算法將整個堆空間劃分成連續的不同小區間。

每一個小區間都獨立使用,獨立回收。這種算法的好處是可以控制一次回收多個小區間。

在這裏插入圖片描述

注意:這些都只是基本的算法思路,實際GC實現過程要複雜得多,目前發展中的前沿GC都是複合算法,並且並行和併發兼備。

4. 垃圾收集器

4.1 垃圾收集器分類

垃圾收集器沒有在規範中進行過多的規定,可以由不同的廠商、不同版本的JVM來實現。由於JDK版本的處於高速迭代過程中,因此java發展至今已經衍生了衆多GC版本。從不同⻆度分析垃圾收集器, 可以將GC分爲不同的類型。

  1. 按線程數分,可以分爲串行垃圾回收器和並行垃圾回收器。

在這裏插入圖片描述

  • 串行回收指的是在同一時間段只允許有一個CPU用於執行垃圾回收操作,此時工作線程被暫停,直至垃圾收集工作結束。
  • ​ 和串行回收相反,並行收集可以運用多個CPU同時執行垃圾回收,因此提升了應用的吞吐􏰂,不過並行回收仍然與串行回收一樣,採用獨佔式,使用了“Stop-the-world”機制。

​ 在諸如單CPU處理器或者較小內存等硬件場合中,串行回收器的性能表現可以超過並行回收器和併發回收器。所以,串行回收默認被應用在客戶端Client模式下的JVM中。

​ 在併發能力較強的CPU上,並行回收器產生的停頓時間要短於串行回收器。

  1. 按照工作模式分,可以分爲併發式垃圾回收器和獨佔式垃圾回收器。
  • 併發式垃圾回收器與應用程序線程交替工作,以儘可能減少應用程序的停頓時間。
  • 獨佔式垃圾回收器一旦運行,就停止應用程序中的所有用戶線程,直到垃圾回收過程完全結束。

在這裏插入圖片描述

  1. 按碎片處理方式分,可分爲壓縮式垃圾回收器和非壓縮式垃圾回收器。
  • 壓縮式垃圾回收器在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片。
  • 非壓縮式的垃圾回收器不進行這不操作,會產生碎片。

在這裏插入圖片描述

  1. 按工作的內存空間分,又可分爲年輕代垃圾回收器和老年代垃圾回收器

4.2 評估GC的性能指標

  • 吞吐量􏰂:運行用戶代碼的時間佔總運行時間的比例 (總運行時間=程序的運行時間+內存回收的時間);
  • 暫停時間:執行垃圾收集時,程序的工作線程被暫停的時間;
  • 內存佔用:java堆區所佔的內存大小;

吞吐量􏰂就是CPU用於運行用戶代碼的時間與CPU總消耗的時間的比值,即吞吐量􏰂=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。比如:虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那麼吞吐􏰂就是99%。這種情況下,應用程序能容忍較高的暫停時間,因此,高吞吐􏰂的應用程序有更⻓的時間基準,快速響應是不必考慮的。

暫停時間是指一個時間段內應用程序線程暫停,讓GC線程執行的狀態。比如:GC期間100毫秒的暫停時間意味這在這100毫秒期間內沒有應用程序線程是活動的。

**注重吞吐量:**吞吐量􏰂優先,意味着在單位時間內,STW的時間最短:0.2 + 0.2= 0.4s

在這裏插入圖片描述
**注重低延遲:**暫停時間優先,意味這儘可能讓單次STW的時間最短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5s

在這裏插入圖片描述

​ 這三者共同構成一個”不可能三⻆“。三者總體的表現會隨着技術進步而越來越好。一款優秀的收集器通常最多同時滿足其中的兩項。簡單來說,主要抓住兩點:

  • 吞吐量
  • 暫停時間

​ 在設計(或使用)GC算法時,必須確定我們的目標:一個GC算法只可能針對兩個目標之一(即只專注於較大吞吐􏰂或最小暫停時間),或嘗試找一個二者的折衷。

現在標準,在最大吞吐量優先的情況下,降低停頓時間。

4.3 垃圾收集器發展史

有了虛擬機,就一定有需要收集垃圾的機制,這就是Garbage Collection,對應的產品我們稱之爲 Garbage Collector

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

4.4 經典垃圾回收器

  • 串行回收器:Serial 、 Serial Old
  • 並行回收器:ParNew、Parallel Scavenge、Parallel Old
  • 併發回收器:CMS、G1

官方文檔參考:

https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

4.5 垃圾回收器的組合關係

​ 如果說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。下圖展示了7種作用於不同分代的收集器,其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用於回收整個Java堆的G1 收集器。不同收集器之間的連線表示它們可以搭配使用。

在這裏插入圖片描述

​ 爲什麼要有很多收集器,一個不夠嗎 ?因爲java的使用場景很多,移動端、服務器等。所以就需要針對不同的場景,提供不同的垃圾收集器,提高垃圾收集的性能。

​ 雖然我們會對各個收集器進行比較,但並非爲了挑選一個最好的收集器出來。沒有一種放之四海而皆準、任何場景下都適用的完美收集器存在,更加沒有萬能的收集器。所以我們選擇的只是對具體應用最合適的收集器。

如何查看默認的垃圾回收器

  • -XX:+PrintCommandLineFlags:查看命令行相關參數(包含使用的垃圾收集器)
  • 使用命令行指令: jinfo -flag 相關垃圾回收參數 進程ID
import java.util.ArrayList;
import java.util.List;
/**
* -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC */
public class GCUseTest {
    public static void main(String[] args) {
			List<byte[]> list = new ArrayList<>(); 
      while (true){
				byte[] arr = new byte[100]; 
      	list.add(arr);
				try {
						Thread.sleep(10);
				} catch (InterruptedException e) {
						e.printStackTrace(); 
     	 	}
      }
    }
}

輸出:


-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=348966912 -XX:MaxTenuringThreshold=6 -XX:OldPLABSize=16 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 

4.6 Serial(複製算法)

​ Serial/Serial Old收集器是最基本最古老的收集器,Serial是JDK1.3之前回收新生代唯一的選擇。它是一個單線程收集器,並且在它進行垃圾收集時,必須暫停所有用戶線程。

​ Serial收集器是作爲HotSpot中Client模式下的默認新生代垃圾收集器,採用的是複製算法。Serial Old收集器是針對老年代的收集器,採用的是標記-整理算法。

​ 如下是 Serial 收集器和 Serial Old 收集器結合進行垃圾收集的示意圖,當用戶線程都執行到安全點時,所有線程暫停執行,Serial 收集器以單線程,採用複製算法進行垃圾收集工作,收集完之後,用戶線程繼續開始執行。它的”單線程“的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更􏰀要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束(Stop the world)。

在這裏插入圖片描述

優點:實現簡單高效(與其他收集器的單線程相比),對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

缺點:會給用戶帶來停頓。

適用場景:Client 模式(桌面應用);單核服務器。

可以用 -XX:+UseSerialGC 參數可以指定年輕代和老年代都使用串行收集器。等價於新生代用 Serial GC,並且老年代用 Serial Old GC。

import java.util.ArrayList; 
import java.util.List;
/**
 * -XX:+UseSerialGC -XX:+PrintCommandLineFlags */
public class GCUserTest {
    public static void main(String[] args){
        List<String> list = new ArrayList<>(); 
        String str = "hey";
        while(true){
            list.add(str); 
            str += str; try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace(); 
            }
        }
    }
}
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC

總結

這種垃圾收集器瞭解即可,現在已經不用串行的了。而且在限定單核CPU纔可以使用,現在都不是單核的了。

對於交互較強的應用而言,這種垃圾收集器是不能接受的。一般在java web應用程序中是不會使用串行垃圾收集器的。

4.7 Serial Old(標記-整理算法)

老年代單線程收集器,Serial收集器的老年代版本;Serial Old是運行在Client模式下默認的老年代的垃圾回收器。Serial Old在server模式下主要有兩個用途:

  1. 與新生代的Parallel Scavenge配合使用 ;

2)作爲老年代CMS收集器的後備垃圾收集方法。

如下圖是 Serial 收集器和 Serial Old 收集器結合進行垃圾收集的示意圖:

在這裏插入圖片描述

4.8 ParNew (複製算法)

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

Par是Parallel的縮寫,New:只能處理的是新生代

新生代收並行集器,ParNew收集器是Serial收集器的多線程版本,使用多個線程進行垃圾收集。在多核CPU環境下有着比Serial更好的表現;ParNew收集器在年輕代中同樣也是採用複製算法、“stop- the-wold”機制。

如下是 ParNew 收集器和 Serial Old 收集器結合進行垃圾收集的示意圖,當用戶線程都執行到安全點時,所有線程暫停執行,ParNew 收集器以多線程,採用複製算法進行垃圾收集工作,收集完之後, 用戶線程繼續開始執行。

在這裏插入圖片描述

  • 對於新生代,回收次數頻繁,使用並行方式高效。
  • 對於老年代,回收次數少,使用串行方式節省資源。(CPU並行需要切換線程,串行可以省去切換線程的資源)。

由於ParNew收集器是基於並行回收,那麼是否可以斷定ParNew收集器的回收效率在任何場景下都會比Serial收集器更高效?(擴展點:多線程程序效率一定高於單線程程序嗎??)

ParNew收集器運行在多CPU的環境下,由於可以充分利用多CPU、多核心等物理硬件資源優勢,可以更快速地完成垃圾收集,提升程序的吞吐􏰂量。

但是在單個CPU的環境下,ParNew收集器不比Serial收集器更高效。雖然Serial收集器是基於串行回收,但是由於CPU不需要頻繁地做任務切換,因此可以有效避免多線程交互過程中產生的一些額外開銷。

適用場景

多核服務器;與 CMS 收集器搭配使用(除Serial外,目前只有ParNew GC能與CMS收集器配合工作)。

參數

​ 當使用 -XX:+UseConcMarkSweepGC 來選擇 CMS 作爲老年代收集器時,新生代收集器默認就是 ParNew,也可以用 -XX:+UseParNewGC 來指定使用 ParNew 作爲新生代收集器。

​ -XX:ParallelGCThreads 限制線程數􏰂,默認開啓和cpu數據相同的線程數。

4.9 Parallel Scavenge(複製算法)

​ HotSpot的年輕代中除了擁有ParNew收集器是基於並行回收的以外,Parallel Scavenge收集器同樣也採用了複製算法、並行回收和“stop the world”機制。

​ 那麼Parallel收集器的出現是否是多此一舉?

  • ​ 和ParNew收集器不同,Parallel Scavenge收集器的目標是達到一個可控制的吞吐量 (Throughput),它也被稱爲吞吐量優先的垃圾收集器。
  • ​ 自適應調節策略也是Parallel Scavenge與ParNew一個重􏰀要區別。

​ 高吞吐量􏰂意味着高效利用 CPU。高吞吐􏰂量可以高效率的利用CPU時間,儘快完成程序的運算任務。

​ 如下是 Parallel 收集器和 Parallel Old 收集器結合進行垃圾收集的示意圖,在新生代,當用戶線程都執行到安全點時,所有線程暫停執行,ParNew 收集器以多線程,採用複製算法進行垃圾收集工作, 收集完之後,用戶線程繼續開始執行;在老年代,當用戶線程都執行到安全點時,所有線程暫停執行, Parallel Old 收集器以多線程,採用標記-整理算法進行垃圾收集工作。

在這裏插入圖片描述

適用場景:

​ 注􏰀重吞吐量􏰂,高效利用 CPU,需要高效運算且不需要太多交互。適合後臺應用等對交互相應要求不高的場景;例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序。

參數配置:

-XX:+UseParallelGC 來選擇 Parallel Scavenge 作爲新生代收集器,

-XX:+UseParallelOldGC 手動指定老年代都是使用並行回收收集器。

  • 分別適用於新生代和老年代。默認jdk8是開啓的。
  • 上面兩個參數,默認開啓一個,另一個也會被開啓。(互相激活)

4.10 Parallel Old (標記-整理算法)

​ 老年代並行收集器,吞吐量􏰂優先,Parallel Scavenge收集器的老年代版本;

在這裏插入圖片描述

​ 適用場景:與Parallel Scavenge收集器搭配使用;注􏰀重吞吐量􏰂。jdk7、jdk8 默認使用該收集器作爲老年代收集器,使用 -XX:+UseParallelOldGC 來指定使用 Paralle Old 收集器。

4.11 CMS(Concurrent Mark Sweep)收集器(標記-清除 算法)

​ 在JDK1.5時,HotSpot推出了一款在強交互應用中􏰀要的一款垃圾收集器: CMS(Concurrent- Mark-Sweep),這款垃圾收集器是HotSpot虛擬機中第一款真正意義上的併發收集器,它第一次實現了 垃圾收集線程與用戶線程同時工作

​ CMS收集器是一種儘可能縮短用戶線程的停頓時間**(低延遲)**收集器,停頓時間越短(低延遲)就越適合與用戶交互的程序,良好的響應速度能提升用戶體驗。

​ 目前很大一部分的java應用集中在B/S系統的服務端上,這類應用尤其􏰀視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

​ 它是一種併發收集器,採用的是標記-清除算法

JDK1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。在G1出現之前,CMS使用還是非常廣泛的。一直到今天,仍然有很多系統使用CMS GC。

4.11.1 收集過程

整個垃圾收集過程分爲 4 個步驟:

  1. 初始標記(Initial-Mark):在這個階段中,程序中所有的用戶線程都會因爲STW機制而出現短暫的暫停,主要任務是標記一下 GC Roots直接關聯到的對象,一旦標記完成就會恢復之前的用戶線程,由於直接關聯對象比較小,所以速度非常快。
  2. 併發標記(Concurrent-Mark):從 GC Roots 的直接關聯對象開始遍歷整個對象圖的過程,標記出全部的垃圾對象,耗時較⻓。這個過程耗時較⻓但是不需要暫停用戶線程,可以與垃圾收集線程一起併發執行。
  3. 新標記(Remark):由於在併發標記階段中,程序的用戶線程和垃圾收集線程同時運行或者交叉運行,因此爲了修正併發標記期間,因用戶線程繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記極端稍⻓一些,但也遠比並發標記階段的時間短。
  4. 併發清除(Current-Sweep):此階段清理刪除標記階段判斷的已經死亡的對象,釋放內存空間。 由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的。

​ 整個過程耗時最⻓的併發標記和併發清除都是和用戶線程一起工作,所以從總體上來說,CMS 收集器垃圾收集可以看做是和用戶線程併發執行的。

在這裏插入圖片描述

​ 儘管CMS收集器採用的是併發回收(非獨佔式),但是在其初始化標記和􏰀新標記這兩個階段中仍然需要執行"stop-the-world"機制暫停程序中的工作線程,不過暫停時間並不太⻓,因此可以說明目前所 有的垃圾收集器都做不到完全不需要"stop-the-world",只是儘可能地縮短暫停時間。

由於最耗費時間的併發標記與併發清除階段都不需要暫停工作,所以整體的回收是低停頓的。

​ 另外,由於在垃圾收集階段用戶線程沒有中斷,所以在CMS回收過程中,還應該確保應用程序用戶線程有足夠的內存可用。因此,CMS收集器不能像其他收集器那樣等到老年代集合完全被填滿了再進行收集,而是當堆內存使用率達到某一閥值時,便開始進行回收。以確保應用程序在CMS工作過程中依然有足夠的空間支持應用程序運行。要是CMS運行期間預留的內存無法滿足程序需要,就會出現一 次"Conrurrent Mode Failure"失敗,這時虛擬機將啓動後備預案:臨時啓動Serial Old****收集器來􏰀新進 行老年的垃圾收集,這樣停頓時間就很⻓了。

4.11.2 空閒列表

​ CMS收集器的垃圾收集算法採用是標記-清除算法,這意味這每次執行完內存回收後,由於被執行內存回收的無用對象所佔用的內存空間極有可能是不連續的一些內存塊,不可能避免的講會產生一些內存碎片。那麼CMS在爲新對象分配內存空間時,將無法使用指針碰撞(Bump the Pointer)技術,而只能夠選擇空閒列表(Free List)執行內存分配。

在這裏插入圖片描述

​ 爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離,這種分配方式稱爲**“指針碰撞”(Bump the Pointer)。如果Java堆中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱爲“空閒列表”**(FreeList)。選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整 又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

​ 因此,在使用Serial、ParNew等帶Compact過程的收集器時,系統採用的分配算法是指針碰撞, 而使用CMS這種基於Mark-Sweep算法的收集器時,通常採用空閒列表。

指針碰撞:

在這裏插入圖片描述

空閒列表:

在這裏插入圖片描述

有人會覺得既然Mark Sweep會造成內存碎片,那麼爲什麼不把算法換成Mark Compact?

答案其實很簡單,因爲當併發清除的時候,用Compact整理內存的話,原來的用戶線程使用的內存還怎麼用?要保證用戶線程能繼續執行,前提是它允許的資源不受影響。

4.11.3 主要優缺點

CMS主要優點:

1.併發收集;

2.低停頓。

CMS明顯的缺點:

  1. **CMS收集器對CPU資源非常敏感。**在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程而導致應用程序變慢,總吞吐量􏰂會降低。

  2. CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC 的產生。由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生, 這部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。

  3. CMS是基於“標記-清除”算法實現的收集器,收集結束時會有大量空間碎片產生。空間碎片過多, 可能會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發FullGC。

4.11.4 常用參數設置

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  1. -XX:+UseConcMarkSweepGC 手動執行使用CMS收集器執行內存回收任務。開啓該參數後會自動將-XX:+UseParNewGC打開。即:ParNew(Young區) + CMS(Old區) + Serial Old的組合

2)-XX:CMSInitiatingOccupancyFraction 設置堆內存使用率的閥值,一旦達到該閥值,便開始進行回收。

-XX:CMSInitiatingOccupancyFraction=20 設置到20%時

如果內存增⻓緩慢,則可以設置一個稍大的值,大的閥值可以有效降低CMS的觸發頻率,檢索老年代回收的次數可以較爲明顯的改善應用程序性能。

相反,如果應用程序內存使用率增⻓很快,則應該降低這個閥值,以避免頻繁觸發老年代串行收集器。通過該選項可以有效降低Full GC的執行次數。

4.11.5 小結

HotSpot這麼多的垃圾回收器,Serial/Serial Old、Parallel GC、CMS這些GC有什麼不同嗎?

  • 如果你想要最小化地使用內存和並行開銷,請選擇Serial Old(老年代) + Serial(年輕代)
  • 如果你想要最大化應用程序的吞吐􏰂,請選擇Parallel Old(老年代) + Parallel(年輕代)
  • 如果你想要最小化GC的中斷或停頓時間,請選擇CMS(老年代) + ParNew(年輕代)

後續版本

JDK9新特性:CMS被標記**廢棄(Deprecate)**了,如果對JDK9 以以上版本的Hotspot虛擬機使用- XX:+UseConcMarkSweepGC參數來開啓CMS收集器的話,用戶會收到一個警告信息,同時CMS在未來將會被廢棄

JDK14新特性:刪除CMS垃圾回收器,移除CMS垃圾收集器,如果在JDK14中使用- XX:+UseConcMarkSweepGC的話,JVM不會報錯,只是給出一個warning信息,但是不會exit,JVM會自動回退以默認GC的方式啓動JVM。

4.12 G1(Garbage First)收集器 (區域化分代式)

既然已經有了前面的幾個強大的GC,爲什麼還要發佈Garbage First(G1)GC ?

​ 原因在於應用程序所對應的業務越來越龐大、複雜、用戶越來越多,沒有GC就不能保證應用程序正常運行,而經常造成STW的GC又跟不上實際的需求,所以纔會不斷地嘗試對GC進行優化。G1垃圾回收器是在java7 update4之後引入的一個新的垃圾回收器,是當前收集器技術發展的最前沿成果之一。 G1收集器基於“標記-整理”算法實現,也就是說不會產生內存碎片。此外,G1收集器不同於之前的收集器的一個􏰀要特點是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。

​ 與此同時,爲了適應現在不斷擴大的內存和不斷增加的處理器數量,進一步降低暫停時間(pause time),同時兼顧良好的吞吐量

​ 官方給G1設定的目標是在延遲可控的情況下獲得儘可能高的吞吐量,所以才擔當起**“全功能收集器”**的􏰀任與期望。

​ G1收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多CPU、多核環境。因此它是一款並行與併發收集器,並且它能建立可預測的停頓時間模型。

4.12.1 爲什麼叫做**Garbage First(G1)**呢?

因爲G1是一個並行回收器,它用堆內存分割爲很多不相關的區域(Region)(物理上是不連續的)。 使用不同的Region來表示Eden、survivor、old等。

在這裏插入圖片描述

​ G1 GC有計劃的避免在整個java堆中進行全區域的垃圾收集。G1 跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),**在後臺維護一個優先列表,每次根據 允許的收集時間,優先回收價值最大的Region

​ 由於這種方式的側􏰀點在於回收垃圾最大􏰂的區間(Region),所以我們給G1一個名字:垃圾優先 (Garbage First)

​ G1是一款面向服務端應用的垃圾收集器,主要針對配備多核CPU及大容􏰂內存的機器,以極大概率滿足GC停頓時間的同時,還兼具高吞吐量的性能特性。

​ 在JDK1.7版本正式啓用,移除了Experimetal的標識,是JDK9以後的默認垃圾回收器,取代了CMS 回收器以及Parallel + ParallelOld 組合。被Oracle官方稱爲**“全功能的垃圾收集器”**。

​ 與此同時,CMS已經在JDK9中被標記爲廢棄(deprecated)。在jdk8中還不是默認的垃圾回收器,需要使用-XX:+UseG1GC來啓用。

4.12.2 G1收集器的優點

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

1) 並行與併發

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

2)分代收集

從分代上看,G1依然屬於分代行垃圾回收器,它會區分年輕代和老年代,年輕代依然有Eden區和 Survivor區。但從堆的結構上,它不要求整個Eden區、Survivor區或者老年代都是聯繫的,也不再堅持 固定大小和固定數􏰂。

將堆空間劃分爲若干個區域**(Region)**,這些區域包含了邏輯上的年輕代和老年代。

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

在這裏插入圖片描述

在這裏插入圖片描述

3) 可預測的停頓時間模型(即:軟實時 soft real time)

G1會通過一個合理的計算模型,計算出每個Region的收集成本並􏰂化,這樣一來,收集器在給定了“停頓”時間限制的情況下,總是能選擇一組恰當的Regions作爲收集目標,讓其收集開銷滿足這個限制 條件,以此達到實時收集的目的。G1收集器之所以能建立可預測的停頓時間模型,是因爲它可以有計劃地避免在整個java堆中進行全區域的垃圾收集。

  • 由於分區原因,G1可以只選取部分區域進行內存回收,這樣縮小了回收的範圍,因此對於全局停頓的發生也能得到較好的控制。
  • G1跟蹤各個Reion裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需的時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。保證了G1收集器在優先的時間內可以獲取儘可能高的收集效率。
  • 相比於CMS GC,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多。

如何建立可靠的停頓預測模型(滿足用戶設定的期望停頓時間)?

G1 收集器的停頓模型是以衰減均值(Decaying Average)爲理論基礎來實現的:垃圾收集過程 中,G1收集器會根據每個 Region 的回收耗時、記憶集中的髒卡數量􏰂等,分析得出平均值、標準偏差等。

“衰減平均值”比普通的平均值更能準確地代表“最近的”平均狀態,通過這些信息預測現在開始回收的話,由哪些 Region 組成回收集才能在不超期望停頓時間的約束下獲得最高收益。

4.12.3 G1收集器的缺點

相對於CMS,G1還不具備全方位、壓倒性優勢。比如在用戶程序運行過程中,G1無論是爲垃圾收集產生的內存佔用還是程序運行時的額外執行負載都要比CMS要高。 從經驗上來說,整體而言:

  • 小內存應用上,CMS 大概率會優於 G1;
  • 大內存應用上,G1 則很可能更勝一籌。

這個臨界點大概是在 6~8G 之間(經驗值)

4.12.4 G1回收器的參數設置

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  • -XX:+UseG1GC 手動指定使用G1收集器執行內存回收任務。
  • ‐XX:G1HeapRegionSize 設置每個Region的大小。值是2的冪,範圍是1MB到32MB之間,目標是根據最小的java堆大小劃分出約2048個區域。默認是堆內存的1/2000。
  • ‐XX:MaxGCPauseMillis 設置期望達到的最大GC停頓時間指標(JVM會盡力實現,但不保證達到)。默認200ms。

4.12.5 如何設置

G1的設計原則就是簡化JVM性能調優,只需要簡單三步即可完成:

  • 第一步: 開啓G1垃圾收集器 (-XX:+UseG1GC)
  • 第二步:設置堆的最大內存( -Xmx -Xms)
  • 第三步:設置最大的停頓時間(‐XX:MaxGCPauseMillis)

4.12.6 收集過程

如下圖所示,G1 收集器收集器收集過程有初始標記、併發標記、最終標記、篩選回收,和 CMS 收集器前幾步的收集過程很相似:

  1. 初始標記:標記出 GC Roots 直接關聯的對象,這個階段速度較快,需要停止用戶線程,單線程執行。
  2. 併發標記:從 GC Root 開始對堆中的對象進行可達性分析,找出存活對象,這個階段耗時較⻓,但可以和用戶線程併發執行。
  3. 最終標記:修正在併發標記階段由於用戶程序執行而產生變動的標記記錄。
  4. 篩選回收:篩選回收階段會對各個 Region 的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來指定回收計劃(用最少的時間來回收包含垃圾最多的區域,這就是 Garbage First 的由來 ——第一時間清理垃圾最多的區塊),這裏爲了提高回收效率,並沒有採用和用戶線程併發執行的方式,而是停頓用戶線程。

在這裏插入圖片描述

4.12.7 G1回收器的使用場景

  1. 面向服務端應用,針對具有大內存、多處理器的機器。(在普通大小的堆裏表現並不驚喜)

  2. 最主要的應用是需要低GC延遲,並具有大堆的應用程序提供解決方案;

3)在堆大小約6GB或更大時,可預測的暫停時間可以低於0.5秒;(G1通過每次只清理一部分而不是全部 Region的增􏰂式清理在保證每次GC停頓時間不會過⻓) 。

4)用來替換掉JDK1.5中的CMS收集器,以下情況,使用G1可能比CMS好

  • 超過50% 的java堆被活動數據佔用;
  • 對象分配頻率或年代提升頻率變化很大;
  • GC停頓時間過⻓(大於0.5至1秒)
  1. HotSpot垃圾收集器裏,除了G1以外,其他的垃圾收集器使用內置的JVM線程執行GC多線程操作, 而G1 GC可以採用應用線程運行GC的工作,即當JVM的GC線程處理速度慢時,系統會調用應用程序幫助加速垃圾回收過程。

4.12.8 Region的使用介紹

分區Region:化整爲零

​ 使用G1收集器是,它將整個java堆劃分爲約2048個大小相同的獨立Region塊,每個Region塊大小 根據堆空間的實際大小而定,整體被控制在1mb到32mb之間,且爲2的N次冪,即1mb、2mb、 4mb、8mb、16mb、32mb。可以通過‐XX:G1HeapRegionSize設定。所有的Region大小相同,且在 JVM生命週期內不會被改變。

雖然還保留這新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分 Region(不需要連續)的集合。通過Region的動態分配方式實現邏輯上的連續。

在這裏插入圖片描述

​ 一個region有可能屬於Eden,Survivor,或者Old 內存區域。但是一個region只可能屬於一個⻆ 色。圖中的E表示region屬於Eden內存區域,S表示屬於Survivor內存區域,O表示屬於Old內存區域。 圖中空白的表示未使用的內存空間。

​ G1垃圾收集器還增加了一種新的內存區域,叫做Humongous內存區域,如圖中的H塊。主要用於 存儲大對象,如果超過1.5個region,就放到H區。

設置H的原因:

​ 對於堆中的大對象,默認直接會分配到老年代,但是如果他是一個短期存在的大對象,就會對垃圾收集器造成負面影響。爲了解決這個問題,G1劃分了一個Humongous區,它用來專⻔存放大對象。**如果一個H區裝不下一個大對象,那麼G1會尋找連續的H區來存儲。**爲了能找到連續的H區,有時候不得不啓動Full GC。G1的大多數行爲都把H區作爲老年代一部分來看待。

4.12.9 主要回收環節

G1 GC的垃圾回收過程主要包含以下三個環節:

  • 年輕代GC (Young GC)
  • 老年代併發標記過程(Concurrent Marking)
  • 混合回收(Mixed GC)**

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

在這裏插入圖片描述

Young GC -> Young GC + concurrent mark -> mixed GC 順序,進行垃圾回收。

年輕代GC

​ 應用程序分配內存,當年輕代的Eden區用盡時開始年輕代回收過程;G1的年輕代收集階段是一個並行的獨佔式收集器。在年輕代回收期,G1 GC暫停所有的應用程序線程,啓動多線程執行年輕代回 收。然後從年輕代區間移動存活對象到Survivor區間或者老年代區間,也有可能是兩個區間都會涉及。

老年代併發標記(Concurrent Marking)

​ 當堆內存使用達到一定值(默認是45%)時,開始老年代併發標記過程。

混合回收(Mixed GC)

​ 標記完成⻢上開始混合回收過程。對於一個混合回收期,G1 GC從老年代移動存活對象到空閒區 間,這些空閒區間也就成爲了老年代的一部分。和年輕代不同,老年代的G1回收器和其他GC不同,G1 的老年代回收器不需要整個老年代被回收,一次主要掃描/回收一小部分老年代Region就可以了。同時,這個老年代Region是和年輕代一起被回收的。

​ 舉個示例:一個Web服務器,java進程最大堆內存爲4G,每分鐘響應1500個請求,每45秒鐘會新 分配大約2G的內存。G1會每45秒鐘進行一次年輕代回收,每31個小時整個堆的使用率會達到45%。會 開始老年代併發標記過程,標記完成後開始四到五次的混合回收。

4.12.10 G1回收器優化建議

1)年輕代大小

  • 固定年輕代的大小會覆蓋暫停時間目標
  • 避免使用-Xmn或者-XX:NewRatio等相關選項顯示設置年輕代大小

2)暫停時間目標不要太過嚴苛

  • 評估G1 GC的吞吐􏰂時,暫停時間目標不要太嚴苛。如果太嚴苛表示你願意承受更多的垃圾回收開銷,而這樣會直接影響吞吐量􏰂
  • G1 GC的吞吐􏰂目標是90%的應用程序時間和10%的垃圾回收時間

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

4.13 垃圾回收器總結

GC 發展階段

Serial => Parallel(並行) => CMS(併發) => G1 => ZGC

​ 截止jdk1.8 ,一共有7款不同垃圾收集器。每一款不同的垃圾收集器都有不同的特點,在具體使用 的時候,需要根據具體的情況選擇不同的垃圾回收器

在這裏插入圖片描述

4.14 ZGC

官方文檔:https://docs.oracle.com/en/java/javase/12/gctuning/

​ ZGC: A Scalable Low-Latency Garbage Collector (Experimental)(ZGC: 可伸縮的低延遲垃圾回收器,處於實驗性階段) http://openjdk.java.net/jeps/333

​ ZGC的目標是:在儘可能對吞吐量影響不大的前提下,實現任意堆內存大小下都可以把垃圾收集的停 頓時間限制控制在十毫秒以內的低延遲。

​ 《深入理解java虛擬機》一書中這樣定義ZGC:ZGC收集器是一款基於Region內存佈局的,(暫時)不設分代的,使用了讀屏障、染色指針的內存多􏰀映射等技術來實現可併發的標記-壓縮算法的,以低延遲爲首要目標的一款垃圾收集器。

​ ZGC的工作過程可以分爲4個階段:併發標記-併發預備重分配-併發重分配-併發重映射等。

​ ZGC幾乎在所有地方都是併發執行的,除了初始標記是STW的。所有停頓時間幾乎就耗費在初始標記上,這部分的實際時間是非常少的。

​ 雖然ZGC還在試驗階段,沒有完成所有特性,但此時性能已經相當亮眼,用**“令人震驚、革命性”**來形容,都不爲過。

​ 未來將在服務端、大內存、低延遲應用的場景下首選垃圾收集器。

​ JDK14之前,ZGC僅在Linux才支持。

​ 儘管許多使用ZGC的用戶都使用類Linux的環境,但在Windows和macOS上,人們也需要ZGC進行開發部署和測試。許多桌面應用也可以從ZGC中受益。因此,ZGC特性被移植到了Windows和macOS 上。

現在mac或Windows上也能使用ZGC了,參數配置如下:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

後續內容請看JVM垃圾回收機制
關注作者不迷路,持續更新高質量Java內容~
原創不易,您的支持/轉發/點贊/評論是我更新的最大動力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章