亞毫秒GC暫停到底有多香?JDK17+ZGC初體驗(yàn)
1、前言
垃圾回收器的暫停問(wèn)題一直是Java工程師關(guān)注的重點(diǎn),特別是對(duì)實(shí)時(shí)響應(yīng)要求較高的服務(wù)來(lái)說(shuō),CMS和G1等主流垃圾回收器的數(shù)十毫秒乃至上百毫秒的暫停時(shí)間相當(dāng)致命。此外,調(diào)優(yōu)門檻也相對(duì)較高,需要對(duì)垃圾回收器的內(nèi)部機(jī)制有一定的了解,才能夠進(jìn)行有效的調(diào)優(yōu)。
為了解決此類問(wèn)題,JDK 11開始推出了一種低延遲垃圾回收器ZGC。ZGC使用了一些新技術(shù)和優(yōu)化算法,可以將GC暫停時(shí)間控制在10毫秒以內(nèi),而在JDK 17的加持下,ZGC的暫停時(shí)間甚至可以控制在亞毫秒級(jí)別!
2、ZGC
ZGC相關(guān)介紹、原理,網(wǎng)上已經(jīng)有很多類似文章,這里只做簡(jiǎn)單介紹。
2.1 設(shè)計(jì)目標(biāo)
ZGC 最初在 JDK 11 中作為實(shí)驗(yàn)性功能引入,并在 JDK 15 中宣布為生產(chǎn)就緒。作為一款低延遲垃圾收集器,旨在滿足以下目標(biāo):
- 8MB到16TB的堆大小支持
- 10ms最大GC暫時(shí)
- 最糟糕的情況下吞吐量會(huì)降低15%(低延時(shí)換吞吐量很值,吞吐量擴(kuò)容即可解決)
2.2 ZGC 內(nèi)存分布
ZGC與傳統(tǒng)的CMS、G1不同、它沒有分代的概念,只有類似G1的Region概率,ZGC 的 Region可以具有如下圖所示的大中下三類容量:
- 小型 Region(Small Region):容量固定為2MB,用于放置小于 256KB的小對(duì)象。
- 中型 Region(Medium Region):容量固定為 32MB,用于放置大于 256KB但是小于 4MB的對(duì)象。
- 大型 Region(Large Region):容量不固定,可以動(dòng)態(tài)變化,但必須為 2MB的整數(shù)倍,用于放置 4MB或以上的大對(duì)象。每個(gè)大型 Region中會(huì)存放一個(gè)大對(duì)象,這也預(yù)示著雖然名字叫“大型 Region”,但它的實(shí)際容量完全有可能小于中型Region,最小容量可低至4MB。大型 Region在ZGC的實(shí)現(xiàn)中是不會(huì)被重分配的(重分配是ZGC的一種處理動(dòng)作,用于復(fù)制對(duì)象的收集器階段)因?yàn)閺?fù)制大對(duì)象的代價(jià)非常高。
2.3 GC工作過(guò)程
與CMS中的ParNew和G1類似,ZGC也采用標(biāo)記-復(fù)制算法,不過(guò)ZGC通過(guò)著色指針和讀屏障技術(shù),解決了轉(zhuǎn)移過(guò)程中準(zhǔn)確訪問(wèn)對(duì)象的問(wèn)題,在標(biāo)記、轉(zhuǎn)移和重定位階段幾乎都是并發(fā)執(zhí)行的,這是ZGC實(shí)現(xiàn)停頓時(shí)間小于10ms目標(biāo)的最關(guān)鍵原因。
從上圖中可以看出,ZGC只有三個(gè)STW階段:初始標(biāo)記,再標(biāo)記,初始轉(zhuǎn)移。
具體轉(zhuǎn)移過(guò)程,網(wǎng)上有大量類似文章,這里不做詳細(xì)介紹,大家有興趣可以參考以下文章:
- 新一代垃圾回收器ZGC的探索與實(shí)踐
- ZGC 最新一代垃圾回收器 | 程序員進(jìn)階
3、為什么選擇JDK17呢?
JDK 17于9月14日發(fā)布,是一個(gè)長(zhǎng)期支持(LTS)版本,這意味著它將在很多年內(nèi)得到支持和更新。這也是第一個(gè)LTS版本,其中包含了一個(gè)可用于生產(chǎn)環(huán)境的ZGC版本。回顧一下,ZGC的實(shí)驗(yàn)版本已經(jīng)包含在JDK 11(之前的LTS版本)中,而第一個(gè)可用于生產(chǎn)環(huán)境的ZGC版本出現(xiàn)在JDK 15(一個(gè)非LTS版本)中。
4、升級(jí)過(guò)程
從JDK8+G1升級(jí)到JDK17+ZGC,主要是在代碼層面和JVM啟動(dòng)參數(shù)層面的做適配。
4.1 JDK下載
首先jdk17選擇的是openjdk,下載地址:https://jdk.java.net/archive/,選擇版本17 GA
4.2 代碼適配
- JDK11移除了 Java EE and CORBA 的模塊
項(xiàng)目中如果用到j(luò)avax.annotation.*、javax.xml.*等等開頭的包,需要手動(dòng)引入對(duì)應(yīng)依賴
<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>
<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相關(guān)依賴版本升級(jí)
<!-- 僅供參考 --><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>
<!-- 僅供參考 -->
<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版本升級(jí)https://projectlombok.org/changelog
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <!-- <version>1.16.20</version>--> <version>1.18.22</version></dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- <version>1.16.20</version>-->
<version>1.18.22</version>
</dependency>
- Java9 模塊化后,不允許應(yīng)用程序查看來(lái)自JDK的所有類,會(huì)影響部分反射的運(yùn)行,需要通過(guò)以下命令解決
--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
--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后啟動(dòng)報(bào)錯(cuò)
在agent后面加上日志輸出即可解決,至于原因,猜測(cè)是跟類加載順序有關(guān)系
-javaagent:/Users/admin/Documents/transmittable-thread-local-2.14.2.jar=ttl.agent.logger:STDOUT
-javaagent:/Users/admin/Documents/transmittable-thread-local-2.14.2.jar
=ttl.agent.logger:STDOUT
以上內(nèi)容僅針對(duì)彩虹橋項(xiàng)目升級(jí)遇到的問(wèn)題,不同的業(yè)務(wù)代碼適配的情況可能不一樣,需要根據(jù)實(shí)際情況尋找解決方案。
4.3 JVM參數(shù)替換
下面是一些通用GC參數(shù)和ZGC特有參數(shù)以及ZGC的一些診斷選型,來(lái)自官網(wǎng):Main - Main - OpenJDK Wiki
通用GC參數(shù) | ZGC特有參數(shù) | ZGC 診斷選型 |
|
|
注意:使用以上參數(shù)需要開啟-XX:+UnlockDiagnosticVMOptions |
具體每個(gè)參數(shù)的含義,這里不做介紹,可參考官網(wǎng)文檔The java Command,里面有詳細(xì)說(shuō)明。
JKD8+G1的啟動(dòng)參數(shù):
-server -Xms36600m -Xmx36600m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+PrintReferenceGC
-XX:+ParallelRefProcEnabled
-XX:G1HeapReginotallow=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的啟動(dòng)參數(shù)如下:
-server -Xms36600m -Xmx36600m#開啟ZGC-XX:+UseZGC #GC周期之間的最大間隔(單位秒)-XX:ZCollectinotallow=120#官方的解釋是 ZGC 的分配尖峰容忍度,數(shù)值越大越早觸發(fā)GC-XX:ZAllocatinotallow=4#關(guān)閉主動(dòng)GC周期,在主動(dòng)回收模式下,ZGC 會(huì)在系統(tǒng)空閑時(shí)自動(dòng)執(zhí)行垃圾回收,以減少垃圾回收在應(yīng)用程序忙碌時(shí)所造成的影響。如果未指定此參數(shù)(默認(rèn)情況),ZGC 會(huì)在需要時(shí)(即堆內(nèi)存不足以滿足分配請(qǐng)求時(shí))執(zhí)行垃圾回收。-XX:-ZProactive #GC日志-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M #發(fā)生OOM時(shí)dump內(nèi)存日志-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/opt/apps/errorDump.hprof
-server -Xms36600m -Xmx36600m
#開啟ZGC
-XX:+UseZGC
#GC周期之間的最大間隔(單位秒)
-XX:ZCollectinotallow=120
#官方的解釋是 ZGC 的分配尖峰容忍度,數(shù)值越大越早觸發(fā)GC
-XX:ZAllocatinotallow=4
#關(guān)閉主動(dòng)GC周期,在主動(dòng)回收模式下,ZGC 會(huì)在系統(tǒng)空閑時(shí)自動(dòng)執(zhí)行垃圾回收,以減少垃圾回收在應(yīng)用程序忙碌時(shí)所造成的影響。如果未指定此參數(shù)(默認(rèn)情況),ZGC 會(huì)在需要時(shí)(即堆內(nèi)存不足以滿足分配請(qǐng)求時(shí))執(zhí)行垃圾回收。
-XX:-ZProactive
#GC日志
-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M
#發(fā)生OOM時(shí)dump內(nèi)存日志
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/apps/errorDump.hprof
5、壓測(cè)結(jié)果
直接上圖
正如 ZGC 設(shè)計(jì)目標(biāo)所描述,它將 GC 暫停時(shí)間從過(guò)去的幾十毫秒降低到了令人驚嘆的亞毫秒級(jí)別。然而,這種超低延遲表現(xiàn)也需要一定的代價(jià),因?yàn)樵趯?shí)現(xiàn)低延遲的同時(shí),ZGC 會(huì)占用一定的 CPU 資源。通常情況下,ZGC 占用的 CPU 比例不會(huì)超過(guò) 15%。在彩虹橋項(xiàng)目中,使用以上推薦的 JVM 參數(shù)后,ZGC 占用的 CPU 資源為 6% 左右。
6、ZGC日志
6.1 輸出ZGC日志
GC日志中包含有關(guān) GC 操作的詳細(xì)信息,可以幫我們分析當(dāng)前GC存在的問(wèn)題。先來(lái)看一下上面JVM參數(shù)中關(guān)于GC日志的參數(shù)
-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M
-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M
- safepoint=trace:記錄關(guān)于 safepoint 的 trace 級(jí)別日志。Safepoint 是 JVM 中一個(gè)特殊的狀態(tài),它用于確保所有線程在特定操作(如垃圾回收、代碼優(yōu)化等)之前進(jìn)入安全狀態(tài)。
- classhisto*=trace:記錄與類的歷史相關(guān)的 trace 級(jí)別日志。
- age*=info:記錄與對(duì)象年齡(在新生代中存在的時(shí)間)相關(guān)的 info 級(jí)別日志。
- gc*=info:記錄與垃圾回收相關(guān)的 info 級(jí)別日志。
- file=/opt/logs/gc-%t.log:將日志寫入到 /opt/logs/ 目錄下的文件中,文件名為 gc-%t.log,其中 %t 是一個(gè)占位符,表示當(dāng)前時(shí)間戳。
- time,level,tid,tags:在每個(gè)日志記錄中包含時(shí)間戳、日志級(jí)別、線程 ID 和標(biāo)簽。
- filesize=50M:設(shè)置日志文件的大小限制為 50MB。當(dāng)日志文件大小達(dá)到此限制時(shí),JVM 將創(chuàng)建一個(gè)新的日志文件并繼續(xù)記錄。
更詳細(xì)的gc日志配置可以參考:https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html#enable-logging-with-the-jvm-unified-logging-framework
6.2 STW關(guān)鍵日志
其中我們重點(diǎn)關(guān)注的就是GC的STW情況,以下是一些關(guān)鍵字代表GC STW階段
- 最基本的STW三階段,初始標(biāo)記:日志中Pause Mark Start,再標(biāo)記:日志中Pause Mark End,初始轉(zhuǎn)移:日志中Pause Relocate Start。
- 內(nèi)存分配阻塞:這一般是因?yàn)槔a(chǎn)速度大于回收速度,垃圾來(lái)不及回收,垃圾將堆占滿時(shí),線程會(huì)阻塞等待GC完成,關(guān)鍵字是Allocation Stall(被阻塞的線程名稱)
如果出現(xiàn)此類日志,可以嘗試如下方法解決:
- -XX:ZCollectionInterval 該配置含義:兩個(gè) GC 周期之間的最大間隔(單位秒)。默認(rèn)情況下,此選項(xiàng)設(shè)置為 0(禁用),可以適當(dāng)調(diào)小該配置,讓GC周期縮短、提升垃圾回收速度,但這會(huì)提升應(yīng)用CPU占用。
- -XX:ZAllocationSpikeTolerance官方的解釋是 ZGC 的分配尖峰容忍度。其實(shí)就是數(shù)值越大,越早觸發(fā)回收。可以適當(dāng)調(diào)大該配置,更早觸發(fā)回收,提升垃圾回收速度,但這會(huì)提升應(yīng)用CPU占用。
- 安全點(diǎn):所有線程進(jìn)入到安全點(diǎn)后才能進(jìn)行GC,ZGC定期進(jìn)入安全點(diǎn)判斷是否需要GC。先進(jìn)入安全點(diǎn)的線程需要等待后進(jìn)入安全點(diǎn)的線程直到所有線程掛起。日志關(guān)鍵字safepoint ... stopped
- dump線程、內(nèi)存:比如jstack、jmap命令,一般是手動(dòng)dump導(dǎo)致,日志關(guān)鍵字HeapDumper
7、Linux大頁(yè)內(nèi)存
在openjdk的官網(wǎng)上也能看到,開啟Linux大頁(yè)內(nèi)存后會(huì)提升應(yīng)用的性能。
開啟方式見官網(wǎng)文檔https://wiki.openjdk.org/display/zgc/Main#Main-EnablingLargePagesOnLinux,注意除了修改系統(tǒng)配置外,還需要在進(jìn)程JVM啟動(dòng)參數(shù)中新增-XX:+UseLargePages配置
經(jīng)過(guò)幾輪壓測(cè)實(shí)際測(cè)試下來(lái),發(fā)現(xiàn)在開啟Linux大頁(yè)后,CPU有8%左右的下降,但是由于大頁(yè)面會(huì)提前預(yù)留指定大小的內(nèi)存,會(huì)導(dǎo)致機(jī)器的內(nèi)存使用率較高。而且目前生產(chǎn)環(huán)境沒有其他應(yīng)用開啟此配置,穩(wěn)定性有待考究,生產(chǎn)環(huán)境自行評(píng)估是否開啟。
8、總結(jié)
在本篇文章中,我們探討了如何升級(jí)到JDK 17,并使用最新一代垃圾回收器ZGC。經(jīng)過(guò)實(shí)踐和測(cè)試,我們發(fā)現(xiàn)升級(jí)后的系統(tǒng)在垃圾回收方面表現(xiàn)出色,暫停時(shí)間被有效控制在1毫秒內(nèi)。盡管這一優(yōu)化過(guò)程可能會(huì)消耗額外的CPU資源,但所獲得的超低GC暫停時(shí)間顯然是非常值得的。總之,相比其他垃圾回收器,ZGC 的性能和穩(wěn)定性已經(jīng)非常優(yōu)秀,而且不需要太多的調(diào)優(yōu)。在大多數(shù)情況下,使用 ZGC官方推薦的默認(rèn)設(shè)置即可獲得優(yōu)秀的性能表現(xiàn)。對(duì)于那些RT敏感型應(yīng)用,升級(jí)到JDK 17并采用ZGC是一個(gè)明智的選擇。