1. 程式人生 > 實用技巧 >20200730 尚矽谷 JVM 17 - 垃圾回收器

20200730 尚矽谷 JVM 17 - 垃圾回收器

17 - 垃圾回收器

1 - GC 分類與效能指標

垃圾回收器概述

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

垃圾回收器分類

  • 執行緒數分,可以分為序列垃圾回收器和並行垃圾回收器。

    • 序列回收指的是在同一時間段內只允許有一個 CPU 用於執行垃圾回收操作,此時工作執行緒被暫停,直至垃圾收集工作結束
      • 在諸如單 CPU 處理器或者較小的應用記憶體等硬體平臺不是特別優越的場合,序列回收器的效能表現可以超過並行回收器和併發回收器。所以,序列回收預設被應用在客戶端的 Client 模式下的 JVM 中
      • 在併發能力比較強的 CPU 上,並行回收器產生的停頓時間要短於序列回收器。
    • 和序列回收相反,並行收集可以運用多個 CPU 同時執行垃圾回收,因此提升了應用的吞吐量,不過並行回收仍然與序列回收一樣,採用獨佔式,使用了 Stop-The-World 機制
  • 按照工作模式分,可以分為併發式垃圾回收器和獨佔式垃圾回收器。

    • 併發式垃圾回收器與應用程式執行緒交替工作,以儘可能減少應用程式的停頓時間。
    • 獨佔式垃圾回收器( Stop the world )一旦執行,就停止應用程式中的所有使用者執行緒,直到垃圾回收過程完全結束。
  • 碎片處理方式分,可分為壓縮式垃圾回收器和非壓縮式垃圾回收器

    • 壓縮式垃圾回收器會在回收完成後,對存活物件進行壓縮整理,消除回收後的碎片。
      • 再分配物件空間使用:指標碰撞
    • 非壓縮式的垃圾回收器不進行這步操作
      • 再分配物件空間使用:空閒列表
  • 工作的記憶體區間分,又可分為年輕代垃圾回收器和老年代垃圾回收器。

評估 GC 的效能指標

  • 吞吐量:執行使用者程式碼的時間佔總執行時間的比例

    • (總執行時間:程式的執行時間 + 記憶體回收的時間)
  • 垃圾收集開銷:吞吐量的補數,垃圾收集所用時間與總執行時間的比例。

  • 暫停時間:執行垃圾收集時,程式的工作執行緒被暫停的時間。

  • 收集頻率:相對於應用程式的執行,收集操作發生的頻率

  • 記憶體佔用:Java 堆區所佔的記憶體大小。

  • 快速:一個物件從誕生到被回收所經歷的時間。

  • 吞吐量、暫停時間、記憶體佔用

    這三者共同構成一個“不可能三角”。三者總體的表現會隨著技術進步而越來越好。一款優秀的收集器通常最多同時滿足其中的兩項。

  • 這三項裡,暫停時間的重要性日益凸顯。因為隨著硬體發展,記憶體佔用多些越來越能容忍,硬體效能的提升也有助於降低收集器執行時對應用程式的影響,即提高了吞吐量。而記憶體的擴大,對延遲反而帶來負面效果

  • 簡單來說,主要抓住兩點:吞吐量、暫停時間

評估 GC 的效能指標:吞吐量( throughput )

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

評估 GC 的效能指標:暫停時間( pause time )

  • “暫停時間”是指一個時間段內應用程式執行緒暫停,讓 GC 執行緒執行的狀態
  • 例如, GC 期間 100 亳秒的暫停時間意味著在這 100 毫秒期間內沒有應用程式執行緒是活動的。
  • 暫停時間優先,意味著儘可能讓單次 STW 的時間最短: 0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5

評估 GC 的效能指標:吞吐量 vs 暫停時間

  • 高吞吐量較好,因為這會讓應用程式的終端使用者感覺只有應用程式執行緒在做“生產性”工作。直覺上,吞吐量越高程式執行越快。

  • 低暫停時間(低延遲)較好,因為從終端使用者的角度來看不管是 GC 還是其他原因導致一個應用被掛起始終是不好的。這取決於應用程式的型別,有時候甚至短暫的 200 毫秒暫停都可能打斷終端使用者體驗。因此,具有低的較大暫停時間是非常重要的,特別是對於一個互動式應用程式

  • 不幸的是”高吞吐量”和”低暫停時間”是一對相互競爭的目標(矛盾)。

    • 因為如果選擇以吞吐量優先,那麼必然需要降低記憶體回收的執行頻率,但是這樣會導致 GC 需要更長的暫停時間來執行記憶體回收。
    • 相反的,如果選擇以低延遲優先為原則,那麼為了降低每次執行記憶體回收時的暫停時間,也只能頻繁地執行記憶體回收,但這又引起了年輕代記憶體的縮減和導致程式吞吐量的下降。
  • 在設計(或使用) GC 演算法時,我們必須確定我們的目標:一個 GC 演算法只可能針對兩個目標之一(即只專注於較大吞吐量或最小暫停時間),或嘗試找到一個二者的折衷。

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

2 - 不同的垃圾回收器概述

垃圾收集機制是 Java 的招牌能力,極大地提高了開發效率。這當然也是面試的熱點那麼, Java 常見的垃圾收集器有哪些?

垃圾收集器發展史

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

  • 1999 年隨 JDK 1.3.1 一起來的是序列方式的 Serial GC ,它是第一款GC 。 ParNew 垃圾收集器是 Serial 收集器的多執行緒版本
  • 2002 年 2 月 26 日, Parallel GC 和 Concurrent Mark Sweep GC 跟隨 JDK 1.4.2 一起釋出
  • Parallel GC 在 JDK 6 之後成為 HotSpot 預設GC 。
  • 2012 年,在 JDK 1.7u4 版本中, G1 可用。
  • 2017 年, JDK 9 中 G1 變成預設的垃圾收集器,以替代 CMS
  • 2018 年 3 月, JDK 10 中 G1 垃圾回收器的並行完整垃圾回收,實現並行性來改善最壞情況下的延遲。

  • 2018 年 9 月, JDK 11 釋出。引入 Epsilon 垃圾回收器,又被稱為 " No-op (無操作)" 回收器。同時,引入 ZGC :可伸縮的低延遲垃圾回收器( Experimental )。
  • 2019 年 3 月, JDK 12 釋出。增強 G1 ,自動返回未用堆記憶體給作業系統。同時,引入 Shenandoah GC :低停頓時間的 GC ( Experimental )。
  • 2019 年 9 月, JDK 13 釋出。增強 ZGC ,自動返回未用堆記憶體給作業系統。
  • 2020 年 3 月, JDK 14 釋出。刪除 CMS 垃圾回收器。擴充套件 ZGC 在 macOS 和 Windows 上的應用

7 款經典的垃圾收集器

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

Memory Management in the Java HotSpot™ Virtual Machine

7 款經典收集器與垃圾分代之間的關係

  • 新生代收集器:Serial、 ParNew、Parallel Scavenge
  • 老年代收集器:Serial Old、Parallel Old、CMS
  • 整堆收集器:G1

垃圾收集器的組合關係

  1. 兩個收集器間有連線,表明它們可以搭配使用: Serial / Serial Old 、 Serial / CMS 、 ParNew / Serial Old 、 ParNew / CMS 、 Parallel Scavenge / Serial Old , Parallel Scavenge / Parallel Old , G1
  2. 其中 Serial Old 作為 CMS 出現 " Concurrent Mode Failure " 失敗的後備預案。
  3. (紅色虛線)由於維護和相容性測試的成本,在 JDK 8 時將 Serial + CMS 、 ParNew + Serial Old 這兩個組合宣告為廢棄( JEP 173 ),並在 JDK 9 中完全取消了這些組合的支援( JEP 214),即:移除。
  4. (綠色虛線)JDK 14 中:棄用 Parallel Scavenge 和 Serial Old GC 組合( JEP 366)
  5. (青色虛線) JDK 14 中:刪除 CMS 垃圾回收器( JEP 363 )
  • 為什麼要有很多收集器,一個不夠嗎?因為 Java 的使用場景很多,移動端,伺服器等。所以就需要針對不同的場景,提供不同的垃圾收集器,提高垃圾收集的效能。
  • 雖然我們會對各個收集器進行比較,但並非為了挑選一個最好的收集器出來。沒有一種放之四海皆準、任何場景下都適用的完美收集器存在,更加沒有萬能的收集器。所以我們選擇的只是對具體應用最合適的收集器

如何檢視預設的垃圾收集器

  • -XX:+PrintCommandLineFlags : 檢視命令列相關引數(包含使用的垃圾收集器)
  • 使用命令列指令 : jinfo -flag 相關垃圾回收器引數 程序ID

3 - Serial 回收器:序列回收

  • Serial 收集器是最基本、歷史最悠久的拉圾收集器了。 JDK 1.3 之前回收新生代唯一的選擇。
  • Serial 收集器作為 HotSpot 中 Client 模式下的預設新生代垃圾收集器。
  • Serial 收集器採用複製演算法、序列回收和 " Stop-the-World ″機制的方式執行記憶體回收。
  • 除了年輕代之外, Serial 收集器還提供用於執行老年代垃圾收集的 Serial Old 收集器。 Serial Old 收集器同樣也採用了序列回收和 " Stop the World " 機制,只不過記憶體回收演算法使用的是標記-壓縮演算法
  • Serial Old 是執行在 Client 模式下預設的老年代的垃圾回收器
  • Serial Old 在 Server 模式下主要有兩個用途:
    • 與新生代的 Parallel Scavenge 配合使用
    • 作為老年代 CMS 收集器的後備垃圾收集方案

這個收集器是一個單執行緒的收集器,但它的“單執行緒”的意義並不僅僅說明它只會使用一個 CPU 或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束( Stop The World )

  • 優勢:簡單而高效(與其他收集器的單執行緒比),對於限定單個 CPU 的環境來說, Serial 收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。
    • 執行在 client 模式下的虛擬機器是個不錯的選擇。
  • 在使用者的桌面應用場景中,可用記憶體一般不大(幾十 MB 至一兩百 MB ),可以在較短時間內完成垃圾收集(幾十 ms 至一百多 ms ),只要不頻繁發生,使用序列回收器是可以接受的。
  • 在 HotSpot 虛擬機器中,使用 -XX:+UseSeria1GC 引數可以指定年輕代和老年代都使用序列收集器。
    • 等價於新生代用 Serial GC ,且老年代用 Serial Old GC

總結

這種垃圾收集器大家瞭解,現在已經不用序列的了。而且在限定單核 CPU 才可以用。現在都不是單核的了。對於互動較強的應用而言,這種垃圾收集器是不能接受的。一般在 Java Web 應用程式中是不會採用序列垃圾收集器的。

4 - ParNew 回收器:並行回收

  • 如果說 Serial GC 是年輕代中的單執行緒垃圾收集器,那麼 ParNew 收集器則是 Serial 收集器的多執行緒版本。
    • Par 是 Parallel 的縮寫, New :只能處理新生代
  • ParNew 收集器除了採用並行回收的方式執行記憶體回收外,兩款垃圾收集器之間幾乎沒有任何區別。 ParNew 收集器在年輕代中同樣也是採用複製演算法、" Stop - the - World " 機制。
  • ParNew 是很多 JVM 執行在 Server 模式下新生代的預設垃圾收集器

  • 對於新生代,回收次數頻繁,使用並行方式高效

  • 對於老年代,回收次數少,使用序列方式節省資源。( CPU 並行需要切換執行緒,序列可以省去切換執行緒的資源)

  • 由於 ParNew 收集器是基於並行回收,那麼是否可以斷定 ParNew 收集器的回收效率在任何場景下都會比 Serial 收集器更高效?

    • ParNew 收集器執行在多 CPU 的環境下,由於可以充分利用多 CPU 多核心等物理硬體資源優勢,可以更快速地完成垃圾收集,提升程式的吞吐量。
    • 但是在單個 CPU 的環境下, ParNew 收集器不比 Serial 收集器更高效。雖然 Serial 收集器是基於序列回收,但是由於 CPU 不需要頻繁地做任務切換,因此可以有效避免多執行緒互動過程中產生的一些額外開銷
  • 因為除 Serial 外,目前只有 ParNew GC 能與 CMS 收集器配合工作

  • 在程式中,開發人員可以通過選項 -XX:+UseParNewGC 手動指定使用 ParNew 收集器執行記憶體回收任務。它表示年輕代使用並行收集器,不影響老年代。

  • -XX:ParallelGCThreads 限制執行緒數量,預設開啟和 CPU 資料相同的執行緒數

5 - Parallel 回收器:吞吐量優先

  • HotSpot 的年輕代中除了擁有 ParNew 收集器是基於並行回收的以外, Parallel Scavenge 收集器同樣也採用了複製演算法、並行回收和 " Stop The World " 機制

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

    • 和 ParNew 收集器不同, Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量( Throughput ),它也被稱為吞吐量優先的垃圾收集器。
    • 自適應調節策略也是 Parallel Scavenge 與 ParNew 一個重要區別。
  • 高吞吐量則可以高效率地利用 CPU 時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。因此,常見在伺服器環境中使用。例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程式

  • Parallel 收集器在 JDK 1.6 時提供了用於執行老年代垃圾收集的 Parallel Old 收集器,用來代替老年代的 Serial Old 收集器。

  • Parallel Old 收集器採用了標記-壓縮演算法,但同樣也是基於並行回收和 " Stop - the - World " 機制

  • 在程式吞吐量優先的應用場景中, Parallel 收集器和 Parallel Old 收集器的組合,在 Server 模式下的記憶體回收效能很不錯

  • 在 Java 8 中,預設是此垃圾收集器

引數配置

  • -XX:+UseParallelGC 手動指定年輕代使用 Parallel 並行收集器執行記憶體回收任務。

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

    • 分別適用於新生代和老年代。預設 JDK 8 中是開啟的。
    • 上面兩個引數,預設開啟一個,另一個也會被開啟。(互相啟用)
  • -XX:ParallelGCThreads 設定年輕代並行收集器的執行緒數。一般地,最好與 CPU 數量相等,以避免過多的執行緒數影響垃圾收集效能。

    • 在預設情況下,當 CPU 數量小於 8 個, ParallelGCThreads 的值等於 CPU 數量。
    • 當 CPU 數量大於 8 個, ParallelGCThreads 的值等於 3 + [ 5 * CPU_Count ] / 8 ]。
  • -XX:MaxGCPauseMillis 設定垃圾收集器最大停頓時間(即 STW 的時間)。單位是毫秒。

  • 為了儘可能地把停頓時間控制在 MaxGCPauseMillis 以內,收集器在工作時會調整 Java 堆大小或者其他一些引數。

  • 對於使用者來講,停頓時間越短體驗越好。但是在伺服器端,我們注重高併發,整體的吞吐量。所以伺服器端適合 Parallel ,進行控制

  • 該引數使用需謹慎。

  • -XX:GCTimeRatio 垃圾收集時間佔總時間的比例(= 1 /( N + 1 ))。用於衡量吞吐量的大小。

    • 取值範圍 (0, 100) 。預設值 99 ,也就是垃圾回收時間不超過 1%
    • 與前一個 -XX:MaxGCPauseMillis 引數有一定矛盾性。暫停時間越長, Ratio 引數就容易超過設定的比例
  • -XX:+UseAdaptiveSizePolicy 設定 Parallel Scavenge 收集器具有自適應調節策略

  • 在這種模式下,年輕代的大小、 Eden 和 Survivor 的比例、晉升老年代的物件年齡等引數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。

  • 在手動調優比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機器的最大堆、目標的吞吐量( GCTimeRatio )和停頓時間( MaxGCPauseMillis ),讓虛擬機器自己完成調優工作。

6 - CMS 回收器:低延遲

  • 在 JDK 1.5 時期, HotSpot 推出了一款在強互動應用中幾乎可認為有劃時代意義的垃圾收集器:CMS ( Concurrent-Mark-Sweep )收集器,這款收集器是 HotSpot 虛擬機器中第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集執行緒與使用者執行緒同時工作。

  • CMS 收集器的關注點是儘可能縮短垃圾收集時使用者執行緒的停頓時間。停頓時間越短(低延遲)就越適合與使用者互動的程式,良好的響應速度能提升使用者體驗。

    • 目前很大一部分的 Java 應用集中在網際網路站或者 B/S 系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗 CMS 收集器就非常符合這類應用的需求。
  • CMS 的垃圾收集演算法採用標記-清除演算法,並且也會 Stop-the-world

  • 不幸的是,CMS 作為老年代的收集器,卻無法與 JDK 1.4.0 中已經存在的新生代收集器 Parallel Scavenge 配合工作,所以在 JDK 1.5 中使用 CMS 來收集老年代的時候,新生代只能選擇 ParNew 或者 Serial 收集器中的一個。

  • 在 G1 出現之前,CMS 使用還是非常廣泛的。一直到今天,仍然有很多系統使用 CMS GC 。

CMS 工作原理

CMS 整個過程比之前的收集器要複雜,整個過程分為 4 個主要階段,即初始標記階段、併發標記階段、重新標記階段和併發清除階段。

  • 初始標記( Initial-Mark )階段:在這個階段中,程式中所有的工作執行緒都將會因為 “ Stop - the - World ” 機制而出現短暫的暫停,這個階段的主要任務僅僅只是標記出 GC Roots 能直接關聯到的物件。一且標記完成之後就會恢復之前被暫停的所有應用執行緒。由於直接關聯物件比較小,所以這裡的速度非常快

  • 併發標記( Concurrent-Mark )階段:從 GC Roots 的直接關聯物件開始遍歷整個物件圖的過程,這個過程耗時較長但是不需要停頓使用者執行緒,可以與垃圾收集執行緒一起併發執行。

  • 重新標記( Remark )階段:由於在併發標記階段中,程式的工作執行緒會和垃圾收集執行緒同時執行或者交叉執行,因此為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短

  • 併發清除( Concurrent-Sweep )階段:此階段清理刪除掉標記階段判斷的已經死亡的物件,釋放記憶體空間。由於不需要移動存活物件,所以這個階段也是可以與使用者執行緒同時併發的

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

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

  • 另外,由於在垃圾收集階段使用者執行緒沒有中斷,所以在 CMS 回收過程中,還應該確保應用程式使用者執行緒有足夠的記憶體可用。因此, CMS 收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,而是當堆記憶體使用率達到某一閾值時,便開始進行回收,以確保應用程式在 CMS 工作過程中依然有足夠的空間攴持應用程式執行。要是 CMS 執行期間預留的記憶體無法滿足程式需要,就會出現一次“ Concurrent Mode Failure ”失敗,這時虛擬機器將啟動後備預案:臨時啟用 Serial Old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。

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

  • 有人會覺得既然 Mark Sweep 會造成記憶體碎片,那麼為什麼不把演算法換成Mark Compact 呢?答案其實很簡答,因為當併發清除的時候,用 Compact 整理記憶體的話,原來的使用者執行緒使用的記憶體還怎麼用呢?要保證使用者執行緒能繼續執行,前提的它執行的資源不受影響嘛。 Mark Compact 更適合“ Stop the World ” 這種場景下使用

  • CMS 的優點

    • 併發收集
    • 低延遲
  • CMS 的弊端

    • 會產生記憶體碎片,導致併發清除後,使用者執行緒可用的空間不足。在無法分配大物件的情況下,不得不提前觸發 Full GC
    • CMS 收集器對 CPU 資源非常敏感。在併發階段,它雖然不會導致使用者停頓,但是會因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量會降低
    • CMS 收集器無法處理浮動垃圾。可能出現 " Concurrent mode Failure " 失敗而導致另一次 Full GC 的產生。在併發標記階段由於程式的工作執行緒和垃圾收集執行緒是同時執行或者交叉執行的,那麼在併發標記階段如果產生新的垃圾物件, CMS 將無法對這些垃圾物件進行標記,最終會導致這些新產生的垃圾物件沒有被及時回收,從而只能在下一次執行 GC 時釋放這些之前未被回收的記憶體空間。

引數配置

  • -XX:+UseConcMarkSweepGC 手動指定使用 CMS 收集器執行記憶體回收任務

    • 開啟該引數後會自動將 -XX:+UseParNewGC 開啟。即: ParNew ( Young 區用)+ CMS ( Old 區用)+ Serial Old 的組合。
  • -XX:+CMSInitiatingOccupanyFraction 設定堆記憶體使用率的閾值,一旦達到該閾值,便開始進行回收。

    • JDK 5 及以前版本的預設值為 68 ,即當老年代的空間使用率達到 68% 時,會執行次 CMS 回收。 JDK 6 及以上版本預設值為 92%
    • 如果記憶體增長緩慢,則可以設定一個稍大的值,大的閾值可以有效降低 CMS 的觸發頻率,減少老年代回收的次數,可以較為明顯地改善應用程式效能。反之,如果應用程式記憶體使用率增長很快,則應該降低這個閾值,以避免頻繁觸發老年代序列收集器。因此通過該選項便可以有效降低 Full GC 的執行次數。
  • -XX:+UseCMSCompactAtFullCollection 用於指定在執行完 Full GC 後對記憶體空間進行壓縮整理,以此避免記憶體碎片的產生。不過由於記憶體壓縮整理過程無法併發執行,所帶來的問題就是停頓時間變得更長了。

  • -XX:CMSFullGCsBeforeCompaction 設定在執行多少次 Full GC 後對記憶體空間進行壓縮整理。

  • -XX:ParallelCMSThreads 設定 CMS 的執行緒數量。

    • CMS 預設啟動的執行緒數是( ParallelGCThreads + 3 )/ 4 , ParallelGCThreads 是年輕代並行收集器的執行緒數。當 CPU 資源比較緊張時,受到 CMS 收集器執行緒的影響,應用程式的效能在垃圾回收階段可能會非常糟糕。

JDK 後續版本中 CMS 的變化

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

小結

HotSpot 有這麼多的垃圾回收器,那麼如果有人問, Serial GC Parallel GC 、 Concurrent Mark Sweep GC 這三個GC 有什麼不同呢?

請記住以下口令:

  • 如果你想要最小化地使用記憶體和並行開銷,請選 Serial GC
  • 如果你想要最大化應用程式的吞吐量,請選 Parallel GC
  • 如果你想要最小化 GC 的中斷或停頓時間,請選 CMS GC 。

7 - G1 回收器:區域化分代式

既然我們已經有了前面幾個強大的 GC ,為什麼還要釋出 Garbage First ( G1 ) GC ?

原因就在於應用程式所應對的業務越來越龐大、複雜,使用者越來越多,沒有 GC 就不能保證應用程式正常進行,而經常造成 STW 的 GC 又跟不上實際的需求,所以才會不斷地嘗試對 GC 進行優化。 G1 ( Garbage-First )垃圾回收器是在 Java 7 update 4 之後引入的一個新的垃圾回收器,是當今收集器技術發展的最前沿成果之一

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

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

為什麼名字叫做 Garbage-First ( G1 )呢?
  • 因為G 1 是一個並行回收器,它把堆記憶體分割為很多不相關的區域( Region )(物理上不連續的)。使用不同的 Region 來表示 Eden 、倖存者 0 區,倖存者 1 區,老年代等。

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

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

  • G1 ( Garbage-First )是一款面向服務端應用的垃圾收集器,主要針對配備多核 CPU 及大容量記憶體的機器,以極高概率滿足 GC 停頓時間的同時,還兼具高吞吐量的效能特徵。

  • 在 JDK 1.7 版本正式啟用,移除了 Experimental 的標識,是 JDK 9 以後的預設垃圾回收器,取代了 CMS 回收器以及 Parallel + Parallel Old 組合。被 Oracle 官方稱為“全功能的垃圾收集器”。

  • 與此同時, CMS 已經在 JDK 9 中被標記為廢棄( deprecated )。在 JDK 8 中還不是預設的垃圾回收器,需要使用 -XX:+UseG1GC 來啟用。

G1 回收器的特點(優勢)

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

  • 並行與併發

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

    • 從分代上看, G1 依然屬於分代型垃圾回收器,它會區分年輕代和老年代,年輕代依然有 Eden 區和 Survivor 區。但從堆的結構上看,它不要求整個 Eden 區、年輕代或者老年代都是連續的,也不再堅持固定大小和固定數量
    • 將堆空間分為若干個區域( Region ),這些區域中包含了邏輯上的年輕代和老年代。
    • 和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;
  • 空間整合

    • CMS :“標記-清除” 演算法、記憶體碎片、若干次 GC 後進行一次碎片整理
    • G1 將記憶體劃分為一個個的 region 。記憶體的回收是以 region 作為基本單位的。 Region 之間是複製演算法,但整體上實際可看作是標記-壓縮( Mark - Compact )演算法,兩種演算法都可以避免記憶體碎片。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次 GC 。尤其是當 Java 堆非常大的時候, G1 的優勢更加明顯。
  • 可預測的停頓時間模型(即:軟實時 soft real - time )

    • 這是 G1 相對於 CMS 的另一大優勢, G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在垃圾收集上的時間不得超過 N 毫秒
    • 由於分割槽的原因, G1 可以只選取部分割槽域進行記憶體回收,這樣縮小了回收的範圍,因此對於全域性停頓情況的發生也能得到較好的控制。
    • G1 跟蹤各個 Region 裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region 。保證了 G1 收集器在有限的時間內可以獲取儘可能高的收集效率。
    • 相比於 CMS GC , G1 未必能做到 CMS 在最好情況下的延時停頓,但是最差情況要好很多。

G1 回收器的缺點

相較於 CMS , G1 還不具備全方位、壓倒性優勢。比如在使用者程式執行過程中,G1 無論是為了垃圾收集產生的記憶體佔用( Footprint )還是程式執行時的額外執行負載( Overload )都要比 CMS 要高。

從經驗上來說,在小記憶體應用上 CMS 的表現大概率會優於 G1 ,而 G1 在大記憶體應用上則發揮其優勢。平衡點在 6 - 8 GB 之間。

G1 回收器的引數設定

  • -XX:+UseG1GC 手動指定使用 G1 收集器執行記憶體回收任務
  • -XX:G1HeapRegionSize 設定每個 Region 的大小。值是 2 的冪,範圍是 1 MB 到 32 MB 之間,目標是根據最小的 Java 堆大小劃分出約 2048 個區域。預設是堆記憶體的 1/2000 。
  • -XX:MaxGCPausemillis 設定期望達到的最大 GC 停頓時間指標( JVM 會盡力實現,但不保證達到)。預設值是 200 ms
  • -XX:ParallelGCThread 設定 STW 時 GC 執行緒數的值。最多設定為 8
  • -XX:ConcGCThreads 設定併發標記的執行緒數。將 n 設定為並行垃圾回收執行緒數( ParallelGCThread )的 1/4 左右。
  • -XX:InitiatingHeapOccupancyPercent 設定觸發併發 GC 週期的 Java 堆佔用率閾值。超過此值,就觸發 GC 。預設值是 45 。

G1 回收器的常見操作步驟

G1 的設計原則就是簡化 JVM 效能調優,開發人員只需要簡單的三步即可完成調優

  1. 開啟 G1 垃圾收集器
  2. 設定堆的最大記憶體
  3. 設定最大的停頓時間

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

G1 回收器的適用場景

  • 面向服務端應用,針對具有大記憶體、多處理器的機器。(在普通大小的堆裡表現並不驚喜)
  • 最主要的應用是需要低 GC 延遲,並具有大堆的應用程式提供解決方案
    • 如:在堆大小約 6 GB 或更大時,可預測的暫停時間可以低於 0.5 秒:( G1 通過每次只清理一部分而不是全部的 Region 的增量式清理來保證每次 GC 停頓時間不會過長)。
  • 用來替換掉 JDK 1.5 中的 CMS 收集器;在下面的情況時,使用 G1 可能比 CMS 好
    • 超過 56% 的 Java 堆被活動資料佔用;
    • 物件分配頻率或年代提升頻率變化很大
    • GC 停頓時間過長(長於 0.5 至 1 秒)。
  • HotSpot 拉圾收集器裡,除了 G1 以外,其他的垃圾收集器使用內建的 JVM 執行緒執行 GC 的多執行緒操作,而 G1 可以採用應用執行緒承擔後臺執行的 GC 工作,即當 JVM 的 GC 執行緒處理速度慢時,系統會呼叫應用程式執行緒幫助加速垃圾回收過程。

分割槽 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 / Tenured 記憶體區域。但是個 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 區作為老年代的一部分來看待。

G1 回收器垃圾回收過程

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

  • 年輕代 GC ( Young GC )
  • 老年代併發標記過程( Concurrent Marking )
  • 混合回收( Mixed GC )
  • (如果需要,單執行緒、獨佔式、高強度的 Full GC 還是繼續存在的。它針對 GC 的評估失敗提供了一種失敗保護機制,即強力回收。)

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

  • 應用程式分配記憶體,當年輕代的 Eden 區用盡時開始年輕代回收過程; G1 的年輕代收集階段是一個並行的獨佔式收集器。在年輕代回收期, G1 GC 暫停所有應用程式執行緒,啟動多執行緒執行年輕代回收。然後從年輕代區間移動存活物件到 Survivor 區間或者老年區間,也有可能是兩個區間都會涉及
  • 當堆記憶體使用達到一定値(預設 45%)時,開始老年代併發標記過程。
  • 標記完成馬上開始混合回收過程。對於一個混合回收期, G1 GC 從老年區間移動存活物件到空閒區間,這些空閒區間也就成為了老年代的一部分。和年輕代不同,老年代的 G1 回收器和其他 GC 不同, G1 的老年代回收器不需要整個老年代被回收,一次只需要掃描/回收小部分老年代的 Region 就可以了。同時,這個老年代 Region 是和年輕代一起被回收的。
  • 舉個例子:一個 Web 伺服器, Java 程序最大堆記憶體為 4G ,每分鐘響應 1500 個請求,每 45 秒鐘會新分配大約 2G 的記憶體。 G1 會每 45 秒鐘進行一次年輕代回收,每 31 個小時整個堆的使用率會達到 45 %,會開始老年代併發標記過程,標記完成後開始四到五次的混合回收。

G1 回收器垃圾回收過程: Remembered Set

  • 一個物件被不同區域引用的問題
    • 一個 Region 不可能是孤立的,一個 Region 中的物件可能被其他任意 Region 中物件引用,判斷物件存活時,是否需要掃描整個 Java 堆才能保證準確?
    • 在其他的分代收集器,也存在這樣的問題(而 G1 更突出)
    • 回收新生代也不得不同時掃描老年代?
    • 這樣的話會降低 Minor GC 的效率
  • 解決方法
    • 無論 G1 還是其他分代收集器, JVM 都是使用 Remembered Set 來避免全域性掃描
    • 每個 Region 都有一個對應的 Remembered Set
    • 每次 Reference 型別資料寫操作時,都會產生一個 Write Barrier 暫時中斷操作;
    • 然後檢查將要寫入的引用指向的物件是否和該 Reference 型別資料在不同的 Region (其他收集器:檢査老年代物件是否引用了新生代物件);
    • 如果不同,通過 CardTable 把相關引用資訊記錄到引用指向物件的所在 Region 對應的 Remembered Set 中
    • 當進行垃圾收集時,在 GC 根節點的列舉範圍加入 Remembered Set ;就可以保證不進行全域性掃描,也不會有遺漏

G1 回收過程一:年輕代 GC

JVM 啟動時, G1 先準備好 Eden 區,程式在執行過程中不斷建立物件到 Eden 區,當 Eden 空間耗盡時, G1 會啟動一次年輕代垃圾回收過程

年輕代垃圾回收只會回收 Eden 區和 Survivor 區。

YGC 時,首先 G1 停止應用程式的執行( Stop - The - World ), G1 建立回收集( Collection Set ),回收集是指需要被回收的記憶體分段的集合,年輕代回收過程的回收集包含年輕代 Eden 區和 Survivor 區所有的記憶體分段。

然後開始如下回收過程:

  1. 掃描根

    根是指 static 變數指向的物件,正在執行的方法呼叫鏈條上的區域性變數等。根引用連同 RSet 記錄的外部引用作為掃描存活物件的入口

  2. 更新 RSet

    處理 dirty card queue 中的 card ,更新 RSet 。此階段完成後, RSet 可以準確的反映老年代對所在的記憶體分段中物件的引用。

    對於應用程式的引用值語句 object field = object1 , JVM 會在之前和之後執行特殊的操作以在 dirty card queues 中入隊一個儲存了物件引用資訊的 card 。在年輕代回收的時候, G1 會對 Dirty Card Queue 中所有的 card 進行處理,以更新 RSet ,保證 RSet 實時準確的反映引用關係。

    那為什麼不在引用賦值語句處直接更新 RSet 呢?這是為了效能的需要, RSet 的處理需要執行緒同步,開銷會很大,使用佇列效能會好很多。

  3. 處理 RSet

    識別被老年代物件指向的 Eden 中的物件,這些被指向的 Eden 中的物件被認為是存活的物件。

  4. 複製物件。

    此階段,物件樹被遍歷, Eden 區記憶體段中存活的物件會被複制到 Survivor 區中空的記憶體分段 Survivor 區記憶體段中存活的物件如果年齡未達閾值,年齡會加 1 ,達到閥值會被複制到 Old 區中空的記憶體分段。如果 Survivor 空間不夠, Eden 空間的部分資料會直接晉升到老年代空間。

  5. 處理引用。

    處理 Soft , Weak , Phantom , Final , JNI Weak 等引用。最終 Eden 空間的資料為空, GC 停止工作,而目標記憶體中的物件都是連續儲存的,沒有碎片,所以複製過程可以達到記憶體整理的效果,減少碎片。

G1 回收過程二:併發標記過程
  1. 初始標記階段:標記從根節點直接可達的物件。這個階段是 STW 的,並且會觸發一次年輕代 GC 。
  2. 根區域掃描( Root Region Scanning ): G1 GC 掃描 Survivor 區直接可達的老年代區域物件,並標記被引用的物件。這一過程必須在 young GC 之前完成。
  3. 併發標記( Concurrent Marking ):在整個堆中進行併發標記(和應用程式併發執行),此過程可能被 young GC 中斷。在併發標記階段,若發現區域物件中的所有物件都是垃圾,那這個區域會被立即回收。同時,併發標記過程中,會計算每個區域的物件活性(區域中存活物件的比例)。
  4. 再次標記( Remark ):由於應用程式持續進行,需要修正上一次的標記結果。是 STW 的。 G1 中採用了比 CMS 更快的初始快照演算法: snapshot - at - the - beginning ( SATB )
  5. 獨佔清理( cleanup ,STW ):計算各個區域的存活物件和 GC 回收比例,並進行排序,識別可以混合回收的區域。為下階段做鋪墊。是 STW 的
    • 這個階段並不會實際上去做垃圾的收集
  6. 併發清理階段:識別並清理完全空閒的區域。
G1 回收過程三:混合回收
  • 當越來越多的物件晉升到老年代 old region 時,為了避免堆記憶體被耗盡,虛擬機器會觸發一個混合的垃圾收集器,即 Mixed GC ,該演算法並不是一個 Old GC ,除了回收整個 Young Region ,還會回收一部分的 Old Region 。這裡需要注意:是一部分老年代,而不是全部老年代。可以選擇哪些 Old Region 進行收集,從而可以對垃圾回收的耗時時間進行控制。也要注意的是 Mixed GC 並不是 Full GC
  • 併發標記結束以後,老年代中百分百為垃圾的記憶體分段被回收了,部分為垃圾的記憶體分段被計算了出來。預設情況下,這些老年代的記憶體分段會分 8 次(可以通過 -XX:G1MixedGCCountTarget 設定)被回收。
  • 混合回收的回收集( Collection Set )包括八分之一的老年代記憶體分段, Eden 區記憶體分段, Survivor 區記憶體分段。混合回收的演算法和年輕代回收的演算法完全一樣,只是回收集多了老年代的記憶體分段。具體過程請參考上面的年輕代回收過程。
  • 由於老年代中的記憶體分段預設分 8 次回收, G1 會優先回收垃圾多的記憶體分段。垃圾佔記憶體分段比例越高的,越會被先回收。並且有一個閾值會決定記憶體分段是否被回收,-XX:G1MixedGCLiveThresholdPercent ,預設為 65%,意思是垃圾佔記憶體分段比例要達到 65% 才會被回收。如果垃圾佔比太低,意味著存活的物件佔比高,在複製的時候會花費更多的時間。
  • 混合回收並不一定要進行 8 次。有一個閾值 -XX:G1HeapWastePercent ,預設值為 10%,意思是允許整個堆記憶體中有 1% 的空間被浪費,意味著如果發現可以回收的垃圾佔堆記憶體的比例低於 10%,則不再進行混合回收。因為 GC 會花費很多的時間但是回收到的記憶體卻很少。
G1 回收可選的過程四: Full GC

G1 的初衷就是要避免 Full GC 的出現。但是如果上述方式不能正常工作, G1 會停止應用程式的執行( Stop - The - World ),使用單執行緒的記憶體回收演算法進行垃圾回收,效能會非常差,應用程式停頓時間會很長。

要避免 Full GC 的發生,一旦發生需要進行調整。什麼時候會發生 Full GC 呢?比如堆記憶體太小,當 G1 在複製存活物件的時候沒有空的記憶體分段可用,則會回退到 Full GC ,這種情況可以通過增大記憶體解決。

導致 G1 Full GC 的原因可能有兩個

  1. Evacuation 的時候沒有足夠的 to - space :存放晉升的物件
  2. 併發處理過程完成之前空間耗盡。

G1 回收過程:補充

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

G1 回收器化建議

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

8 - 垃圾回收器總結

7 種經典垃圾回收器總結

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

垃圾收集器 分類 作用位置 使用演算法 特點 適用場景
Serial 序列執行 作用於新生代 複製演算法 響應速度優先 適用於單 CPU 環境下的 Client 模式
ParNew 並行執行 作用於新生代 複製演算法 響應速度優先 多 CPU 環境 Server 模式下與 CMS 配合使用
Parallel 並行執行 作用於新生代 複製演算法 吞吐量優先 適用於後臺運算而不需要太多互動的場景
Serial Old 序列執行 作用於老年代 標記-壓縮演算法 響應速度優先 適用於單 CPU 環境下的 Client 模式
Parallel Old 並行執行 作用於老年代 標記-壓縮演算法 吞吐量優先 適用於後臺運算而不需要太多互動的場景
CMS 併發執行 作用於老年代 標記-清除演算法 響應速度優先 適用於網際網路或 B/S 業務
G1 併發、並行執行 作用於新生代、老年代 標記-壓縮演算法、複製演算法 響應速度優先 面向服務端應用

垃圾回收器組合

不同廠商、不同版本的虛擬機器實現差別很大。 HotSpot 虛擬機器在 JDK 7 / 8 後所有收集器及組合(連線),如下圖:(更新到了 JDK 14 )

怎麼選擇垃圾回收器?

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

  • 怎麼選擇垃圾收集器?

    1. 優先調整堆的大小讓 JVM 自適應完成。
    2. 如果記憶體小於 100M ,使用序列收集器
    3. 如果是單核、單機程式,並且沒有停頓時間的要求,序列收集器
    4. 如果是多 CPU 、需要高吞吐量、允許停頓時間超過 1 秒,選擇並行或者 JVM 自由選擇
    5. 如果是多 CPU 、追求低停頓時間,需快速響應(比如延遲不能超過 1 秒,如網際網路應用),使用併發收集器官方推薦 G1 ,效能高。現在網際網路的專案,基本都是使用 G1
  • 最後需要明確一個觀點

    1. 沒有最好的收集器,更沒有萬能的收集
    2. 調優永遠是針對特定場景、特定需求,不存在一勞永逸的收集器

9 - GC 日誌分析

通過閱讀 GC 日誌,我們可以瞭解 Java 虛擬機器記憶體分配與回收策略。

記憶體分配與垃圾回收的引數列表

  • -XX:+PrintGC 輸出 GC 日誌。類似:-verbose:gc

  • -XX:+PrintGCDetails 輸出 GC 的詳細日誌

  • -XX:+PrintGCTimeStamps 輸出 GC 的時間戳(以基準時間的形式)

  • -XX:+PrintGCDateStamps 輸出 GC 的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800 )

  • -XX:+PrintHeapAtGC 在進行 GC 的前後打印出堆的資訊

  • -Xloggc:/logs/gc.log 日誌檔案的輸出路徑

  • 開啟GC日誌:

    -verbose:gc
    

    這個只會顯示總的GC堆的變化,如下

    [GC (Allocation Failure)  15262K->13758K(58880K), 0.0034064 secs]
    [GC (Allocation Failure)  29058K->29024K(58880K), 0.0038289 secs]
    [Full GC (Ergonomics)  29024K->28766K(58880K), 0.0087412 secs]
    [Full GC (Ergonomics)  44076K->43669K(58880K), 0.0046676 secs]
    
    • GC 、 Full GC : GC 的型別, GC 只在新生代上進行, Full GC 包括永生代,新生代,老年代。
    • Allocation Failure : GC 發生的原因。 80832K -> 19298K :堆在 GC 前的大小和 GC 後的大小。
    • 17920K :現在的堆總大小。
    • 0.0084018 secs : GC 持續的時間。
  • 開啟 GC 日誌:

    -verbose:gc -XX:+PrintGCDetails
    

    輸入資訊如下:

    [GC (Allocation Failure) [PSYoungGen: 15262K->2540K(17920K)] 15262K->13758K(58880K), 0.0035055 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [PSYoungGen: 17840K->2552K(17920K)] 29058K->28872K(58880K), 0.0044184 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC (Ergonomics) [PSYoungGen: 2552K->0K(17920K)] [ParOldGen: 26320K->28766K(40960K)] 28872K->28766K(58880K), [Metaspace: 3467K->3467K(1056768K)], 0.0102481 secs] [Times: user=0.03 sys=0.02, real=0.01 secs] 
    [Full GC (Ergonomics) [PSYoungGen: 15309K->3000K(17920K)] [ParOldGen: 28766K->40668K(40960K)] 44076K->43669K(58880K), [Metaspace: 3468K->3468K(1056768K)], 0.0046369 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    Heap
     PSYoungGen      total 17920K, used 10253K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
      eden space 15360K, 66% used [0x00000000fec00000,0x00000000ff603410,0x00000000ffb00000)
      from space 2560K, 0% used [0x00000000ffd80000,0x00000000ffd80000,0x0000000100000000)
      to   space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
     ParOldGen       total 40960K, used 40668K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)
      object space 40960K, 99% used [0x00000000fc400000,0x00000000febb73c8,0x00000000fec00000)
     Metaspace       used 3474K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 381K, capacity 388K, committed 512K, reserved 1048576K
    
    • GC , Full GC :同樣是 GC 的型別
    • Allocation Failure : GC 原因
    • PSYoungGen :使用了 Parallel Scavenge 並行垃圾收集器的新生代 GC 前後大小的變化
    • ParOldGen :使用了 Parallel Old 並行垃圾收集器的老年代 GC 前後大小的變化
    • Metaspace :元資料區 GC 前後大小的變化, JDK 1.8 中引入了元資料區以替代永久代
    • xxx secs :指 GC 花費的時間
    • Times :
      • user :指的是垃圾收集器花費的所有 CPU 時間
      • sys :花費在等待系統呼叫或系統事件的時間
      • real :GC 從開始到結束的時間,包括其他程序佔用時間片的實際時間。
  • 開啟GC日誌

    -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
    

    輸入資訊如下

    2020-07-30T11:22:32.509+0800: 0.123: [GC (Allocation Failure) [PSYoungGen: 15262K->2552K(17920K)] 15262K->13702K(58880K), 0.0033793 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    2020-07-30T11:22:32.515+0800: 0.128: [GC (Allocation Failure) [PSYoungGen: 17852K->2556K(17920K)] 29002K->29016K(58880K), 0.0038081 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    2020-07-30T11:22:32.519+0800: 0.132: [Full GC (Ergonomics) [PSYoungGen: 2556K->0K(17920K)] [ParOldGen: 26460K->28766K(40960K)] 29016K->28766K(58880K), [Metaspace: 3470K->3470K(1056768K)], 0.0087084 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    2020-07-30T11:22:32.529+0800: 0.142: [Full GC (Ergonomics) [PSYoungGen: 15309K->3000K(17920K)] [ParOldGen: 28766K->40668K(40960K)] 44076K->43669K(58880K), [Metaspace: 3470K->3470K(1056768K)], 0.0054738 secs] [Times: user=0.13 sys=0.00, real=0.01 secs] 
    Heap
     PSYoungGen      total 17920K, used 10253K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
      eden space 15360K, 66% used [0x00000000fec00000,0x00000000ff603410,0x00000000ffb00000)
      from space 2560K, 0% used [0x00000000ffd80000,0x00000000ffd80000,0x0000000100000000)
      to   space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
     ParOldGen       total 40960K, used 40668K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)
      object space 40960K, 99% used [0x00000000fc400000,0x00000000febb73c8,0x00000000fec00000)
     Metaspace       used 3476K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 381K, capacity 388K, committed 512K, reserved 1048576K
    
    • 說明:帶上了日期和時間
  • 如果想把GC日誌存到檔案的話,是下面這個引數

    -Xloggc:/path/to/gc.log
    

    可以用一些工具去分析這些 GC 日誌。

    常用的日誌分析工具有: GCViewer、 GCEasy、 GCHisto、 GCLogViewer、Hpjmeter、 garbagecat 等。

  • [GC[Full GC 說明了這次垃圾收集的停頓型別,如果有" Full "則說明 GC 發生了" Stop The World

  • 使用 Serial 收集器在新生代的名字是 Default New Generation ,因此顯示的是 [DefNew

  • 使用 ParNew 收集器在新生代的名字會變成 [ParNew ,意思是" Parallel New Generation "

  • 使用 Parallel Scavenge 收集器在新生代的名字是 [PSYoungGen

  • 老年代的收集和新生代道理一樣,名字也是收集器決定的

  • 使用 G1 收集器的話,會顯示為 garbagefirst heap

  • Allocation Failure 表明本次引起 GC 的原因是因為在年輕代中沒有足夠的空間能夠儲存新的資料了。

  • [PSYoungGen: 15262K->2552K(17920K)] 15262K->13702K(58880K)

    • 中括號內: GC 回收前年輕代大小,回收後大小,(年輕代總大小)
    • 括號外: GC 回收前年輕代和老年代大小,回收後大小,(年輕代和老年代總大小)
  • user 代表使用者態回收耗時, sys 核心態回收耗時, real 實際耗時。由於多核的原因,時間總和可能會超過 real 時間

Young GC

Full GC

10 - 垃圾回收器的新發展

GC 仍然處於飛速發展之中,目前的預設選項 G1 GC 在不斷的進行改進,很多我們原來認為的缺點,例如序列的 Full GC 、 Card Table 掃描的低效等,都已經被大幅改進,例如, JDK 10 以後, Full GC 已經是並行執行,在很多場景下,其表現還略優於 Parallel GC 的並行 Full GC 實現。

即使是 Serial GC ,雖然比較古老,但是簡單的設計和實現未必就是過時的,它本身的開銷,不管是 GC 相關資料結構的開銷,還是執行緒的開銷,都是非常小的,所以隨著雲端計算的興起,在 Serverless 等新的應用場景下, Serial GC 找到了新的舞臺

比較不幸的是 CMS GC ,因為其演算法的理論缺陷等原因,雖然現在還有非常大的使用者群體,但在 JDK 9 中已經被標記為廢棄,並在 JDK 14 版本中移除。

JDK 11 新特性
  • JEP318

    Epsilon

  • JEP333

    ZGC(Experimental)

Open JDK 12 的 Shenandoah GC

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

Shenandoah ,無疑是眾多 GC 中最孤獨的一個。是第一款不由 Oracle 公司團隊領導開發的 HotSpot 垃圾收集器。不可避免的受到官方的排擠。比如號稱 Open JDK 和 Oracle JDK 沒有區別的 Oracle 公司仍拒絕在 Oracle JDK 12 中支援 Shenandoah 。

Shenandoah 垃圾回收器最初由 RedHat 進行的一項垃圾收集器研究專案 Pauseless GC 的實現,旨在針對 JVM 上的記憶體回收實現低停頓的需求。在 2014 年貢獻給 Open JDK 。

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

Shenandoah GC 的弱項:高執行負擔下的吞吐量下降。

Shenandoah GC 的強項:低延退時間。

令人震驚、革命性的 ZGC

HotSpot Virtual Machine Garbage Collection Tuning Guide

ZGC 與 Shenandoah 目標高度相似,在儘可能對吞吐量影響不大的前提下實現在任意堆記憶體大小下都可以把垃圾收集的停頓時間限制在十毫秒以內的低延遲

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

ZGC 的工作過程可以分為 4 個階段:併發標記 → 併發預備重分配 → 併發重分配 → 併發重對映等。

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

雖然 ZGC 還在試驗狀態,沒有完成所有特性,但此時效能已經相當亮眼,用“令人震驚革命性”來形容,不為過。

未來將在服務端、大記憶體、低延遲應用的首選垃圾收集器。

JDK 14 之前, ZGC 僅 Linux 才支援

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

現在 mac 或 Windows 上也能使用 ZGC 了,示例如下: -XX:+UnlockExperimentalVMOptions -XX:+UseZGC

其他廠商的垃圾回收期

  • AliGC 是阿里巴巴 JVM 團隊基於 G1 演算法,面向大堆( LargeHeap )應用場景
  • Zing,比較有名的低延遲 GC