博客 / 詳情

返回

亞毫秒 GC 暫停到底有多香?JDK17+ZGC 初體驗|得物技術

1 前言

垃圾回收器的暫停問題一直是Java工程師關注的重點,特別是對實時響應要求較高的服務來説,CMS和G1等主流垃圾回收器的數十毫秒乃至上百毫秒的暫停時間相當致命。此外,調優門檻也相對較高,需要對垃圾回收器的內部機制有一定的瞭解,才能夠進行有效的調優。
為了解決此類問題,JDK 11開始推出了一種低延遲垃圾回收器ZGC。ZGC使用了一些新技術和優化算法,可以將GC暫停時間控制在10毫秒以內,而在JDK 17的加持下,ZGC的暫停時間甚至可以控制在亞毫秒級別!

2 ZGC

ZGC相關介紹、原理,網上已經有很多類似文章,這裏只做簡單介紹。

2.1 設計目標

ZGC 最初在 JDK 11 中作為實驗性功能引入,並在 JDK 15 中宣佈為生產就緒。作為一款低延遲垃圾收集器,旨在滿足以下目標:

  • 8MB到16TB的堆大小支持
  • 10ms最大GC暫時
  • 最糟糕的情況下吞吐量會降低15%(低延時換吞吐量很值,吞吐量擴容即可解決)

1.png

2.2 ZGC 內存分佈

ZGC與傳統的CMS、G1不同、它沒有分代的概念,只有類似G1的Region概率,ZGC 的 Region可以具有如下圖所示的大中下三類容量:

  • 小型 Region(Small Region):容量固定為2MB,用於放置小於 256KB的小對象。
  • 中型 Region(Medium Region):容量固定為 32MB,用於放置大於 256KB但是小於 4MB的對象。
  • 大型 Region(Large Region):容量不固定,可以動態變化,但必須為 2MB的整數倍,用於放置 4MB或以上的大對象。每個大型 Region中會存放一個大對象,這也預示着雖然名字叫“大型 Region”,但它的實際容量完全有可能小於中型Region,最小容量可低至4MB。大型 Region在ZGC的實現中是不會被重分配的(重分配是ZGC的一種處理動作,用於複製對象的收集器階段)因為複製大對象的代價非常高。

2.png

2.3 GC工作過程

與CMS中的ParNew和G1類似,ZGC也採用標記-複製算法,不過ZGC通過着色指針和讀屏障技術,解決了轉移過程中準確訪問對象的問題,在標記、轉移和重定位階段幾乎都是併發執行的,這是ZGC實現停頓時間小於10ms目標的最關鍵原因。

3.png

從上圖中可以看出,ZGC只有三個STW階段:初始標記,再標記,初始轉移。
具體轉移過程,網上有大量類似文章,這裏不做詳細介紹,大家有興趣可以參考以下文章:

新一代垃圾回收器ZGC的探索與實踐
ZGC 最新一代垃圾回收器 | 程序員進階

3 為什麼選擇JDK17呢?

JDK 17於9月14日發佈,是一個長期支持(LTS)版本,這意味着它將在很多年內得到支持和更新。這也是第一個LTS版本,其中包含了一個可用於生產環境的ZGC版本。回顧一下,ZGC的實驗版本已經包含在JDK 11(之前的LTS版本)中,而第一個可用於生產環境的ZGC版本出現在JDK 15(一個非LTS版本)中。

4 升級過程

從JDK8+G1升級到JDK17+ZGC,主要是在代碼層面和JVM啓動參數層面的做適配。

4.1 JDK下載

首先jdk17選擇的是openjdk,下載地址:https://jdk.java.net/archive/,選擇版本17 GA

4.png

4.2 代碼適配

  • JDK11移除了 Java EE and CORBA 的模塊

項目中如果用到javax.annotation.、javax.xml.等等開頭的包,需要手動引入對應依賴

<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
</dependency>
  • maven相關依賴版本升級
<!-- 僅供參考 -->
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version>
<maven-resources-plugin.version>3.2.0</maven-resources-plugin.version>
<maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
<maven-deploy-plugin.version>3.0.0-M1</maven-deploy-plugin.version>
<maven-release-plugin.version>3.0.0-M1</maven-release-plugin.version>
<maven-site-plugin.version>3.9.1</maven-site-plugin.version>
<maven-enforcer-plugin.version>3.0.0-M2</maven-enforcer-plugin.version>
<maven-project-info-reports-plugin.version>3.1.0</maven-project-info-reports-plugin.version>
<maven-plugin-plugin.version>3.6.1</maven-plugin-plugin.version>
<maven-javadoc-plugin.version>3.3.0</maven-javadoc-plugin.version>
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
<maven-jxr-plugin.version>3.0.0</maven-jxr-plugin.version>
  • Lombok版本升級https://projectlombok.org/changelog
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
   <!-- <version>1.16.20</version>-->
    <version>1.18.22</version>
</dependency>

5.png

  • Java9 模塊化後,不允許應用程序查看來自JDK的所有類,會影響部分反射的運行,需要通過以下命令解決
--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
  • 本地使用了transmittable-thread-local-2.14.2.jar後啓動報錯

6.png

在agent後面加上日誌輸出即可解決,至於原因,猜測是跟類加載順序有關係

-javaagent:/Users/admin/Documents/transmittable-thread-local-2.14.2.jar
=ttl.agent.logger:STDOUT

以上內容僅針對彩虹橋項目升級遇到的問題,不同的業務代碼適配的情況可能不一樣,需要根據實際情況尋找解決方案。

4.3 JVM參數替換

下面是一些通用GC參數和ZGC特有參數以及ZGC的一些診斷選型,來自官網:Main - Main - OpenJDK Wiki

13.png

具體每個參數的含義,這裏不做介紹,可參考官網文檔The java Command,裏面有詳細説明。

JKD8+G1的啓動參數:

-server -Xms36600m -Xmx36600m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+PrintReferenceGC
-XX:+ParallelRefProcEnabled
-XX:G1HeapRegionSize=16m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/apps/errorDump.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-XX:+PrintGCApplicationConcurrentTime
-verbose:gc
-Xloggc:/opt/apps/logs/${app_name}-gc.log

JDK17+ZGC的啓動參數如下:

-server -Xms36600m -Xmx36600m
#開啓ZGC
-XX:+UseZGC 
#GC週期之間的最大間隔(單位秒)
-XX:ZCollectionInterval=120
#官方的解釋是 ZGC 的分配尖峯容忍度,數值越大越早觸發GC
-XX:ZAllocationSpikeTolerance=4
#關閉主動GC週期,在主動回收模式下,ZGC 會在系統空閒時自動執行垃圾回收,以減少垃圾回收在應用程序忙碌時所造成的影響。如果未指定此參數(默認情況),ZGC 會在需要時(即堆內存不足以滿足分配請求時)執行垃圾回收。
-XX:-ZProactive 
#GC日誌
-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M 
#發生OOM時dump內存日誌
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/apps/errorDump.hprof

5 壓測結果

直接上圖

7.png

8.png

9.png

正如 ZGC 設計目標所描述,它將 GC 暫停時間從過去的幾十毫秒降低到了令人驚歎的亞毫秒級別。然而,這種超低延遲表現也需要一定的代價,因為在實現低延遲的同時,ZGC 會佔用一定的 CPU 資源。通常情況下,ZGC 佔用的 CPU 比例不會超過 15%。在彩虹橋項目中,使用以上推薦的 JVM 參數後,ZGC 佔用的 CPU 資源為 6% 左右。

6 ZGC日誌

6.1 輸出ZGC日誌

GC日誌中包含有關 GC 操作的詳細信息,可以幫我們分析當前GC存在的問題。先來看一下上面JVM參數中關於GC日誌的參數

-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M

  • safepoint=trace:記錄關於 safepoint 的 trace 級別日誌。
    Safepoint 是 JVM 中一個特殊的狀態,它用於確保所有線程在特定操作(如垃圾回收、代碼優化等)之前進入安全狀態。
  • classhisto*=trace:記錄與類的歷史相關的 trace 級別日誌。
    age*=info:記錄與對象年齡(在新生代中存在的時間)相關的 info 級別日誌。
  • gc*=info:記錄與垃圾回收相關的 info 級別日誌。
  • file=/opt/logs/gc-%t.log:將日誌寫入到 /opt/logs/ 目錄下的文件中,文件名為 gc-%t.log,其中 %t 是一個佔位符,表示當前時間戳。
  • time,level,tid,tags:在每個日誌記錄中包含時間戳、日誌級別、線程 ID 和標籤。
  • filesize=50M:設置日誌文件的大小限制為 50MB。當日志文件大小達到此限制時,JVM 將創建一個新的日誌文件並繼續記錄。

更詳細的gc日誌配置可以參考:https://docs.oracle.com/en/java/javase/17/docs/specs/man/java...

6.2 STW關鍵日誌

其中我們重點關注的就是GC的STW情況,以下是一些關鍵字代表GC STW階段

  • 最基本的STW三階段,初始標記:日誌中Pause Mark Start,再標記:日誌中Pause Mark End,初始轉移:日誌中Pause Relocate Start。

10.png

  • 內存分配阻塞:這一般是因為垃圾生產速度大於回收速度,垃圾來不及回收,垃圾將堆佔滿時,線程會阻塞等待GC完成,關鍵字是Allocation Stall(被阻塞的線程名稱)

11.jpeg

如果出現此類日誌,可以嘗試如下方法解決:

  1. -XX:ZCollectionInterval 該配置含義:兩個 GC 週期之間的最大間隔(單位秒)。默認情況下,此選項設置為 0(禁用),可以適當調小該配置,讓GC週期縮短、提升垃圾回收速度,但這會提升應用CPU佔用。
  2. -XX:ZAllocationSpikeTolerance官方的解釋是 ZGC 的分配尖峯容忍度。其實就是數值越大,越早觸發回收。可以適當調大該配置,更早觸發回收,提升垃圾回收速度,但這會提升應用CPU佔用。
  • 安全點:所有線程進入到安全點後才能進行GC,ZGC定期進入安全點判斷是否需要GC。先進入安全點的線程需要等待後進入安全點的線程直到所有線程掛起。日誌關鍵字safepoint ... stopped
  • dump線程、內存:比如jstack、jmap命令,一般是手動dump導致,日誌關鍵字HeapDumper

7 Linux大頁內存

在openjdk的官網上也能看到,開啓Linux大頁內存後會提升應用的性能。

12.png

開啓方式見官網文檔https://wiki.openjdk.org/display/zgc/Main#Main-EnablingLargeP...,注意除了修改系統配置外,還需要在進程JVM啓動參數中新增-XX:+UseLargePages配置

經過幾輪壓測實際測試下來,發現在開啓Linux大頁後,CPU有8%左右的下降,但是由於大頁面會提前預留指定大小的內存,會導致機器的內存使用率較高。而且目前生產環境沒有其他應用開啓此配置,穩定性有待考究,生產環境自行評估是否開啓。

8 總結

在本篇文章中,我們探討了如何升級到JDK 17,並使用最新一代垃圾回收器ZGC。經過實踐和測試,我們發現升級後的系統在垃圾回收方面表現出色,暫停時間被有效控制在1毫秒內。儘管這一優化過程可能會消耗額外的CPU資源,但所獲得的超低GC暫停時間顯然是非常值得的。總之,相比其他垃圾回收器,ZGC 的性能和穩定性已經非常優秀,而且不需要太多的調優。在大多數情況下,使用 ZGC官方推薦的默認設置即可獲得優秀的性能表現。對於那些RT敏感型應用,升級到JDK 17並採用ZGC是一個明智的選擇。

文: 新一

本文屬得物技術原創,來源於:得物技術官網

未經得物技術許可嚴禁轉載,否則依法追究法律責任!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.