1、前言
如果你仍在使用JDK8,那你是否曾經(jīng)遇到過OutOfMemoryError的問題?你是否曾經(jīng)為JVM的調(diào)優(yōu)問題感到困擾?本篇文章將為你介紹一種能夠提供百倍性能提升的垃圾回收器,也許能夠解決你的問題。
上篇文章給大家?guī)砹薐DK8升級JDK11的最全實(shí)踐,相信大家閱讀后已經(jīng)對JDK11有了比較深入的了解。2021年9月14日,Oracle發(fā)布了可以長期支持的JDK17版本,那么從JDK11到JDK17,到底帶來了哪些特性呢?亞毫秒級的ZGC效果到底怎么樣呢?值得我們升級嗎?而且升級過程會遇到哪些問題呢?帶著這些問題,本篇文章將帶來完整的JDK11升級JDK17最全實(shí)踐。
2、為什么升級JDK17
1)長期支持版本
JDK17是Oracle官方在2021年9月14日發(fā)布的一個長期支持(LTS)版本,意味著它將獲得長期的更新和支持,有助于保持程序的穩(wěn)定性和可靠性。
2)性能提升
更好的垃圾回收器。綜合評估,從Java 8 升級到 Java 11,G1GC平均速度提升16.1%,ParallelGC為4.5%,從Java 11 升級到 Java 17,G1GC平均速度提升8.66%,ParallelGC為6.54%(基于OptaPlanner的用例基準(zhǔn)測試表明)?
最大的亮點(diǎn)是帶來了穩(wěn)定版的ZGC垃圾回收器,達(dá)到亞毫秒級停頓。
3)新語法和特性
Switch表達(dá)式簡化、Text Blocks文本塊、instanceof 的模式匹配升級和NullPointerException提示信息改進(jìn)等
4)支持最新的技術(shù)和框架
Spring framework6 和Spring Boot3 都默認(rèn)使用 Java 17作為最低版本
?
3、升級后壓測效果
先給出結(jié)論: 1、JDK17相對于JDK8和JDK11,所有垃圾回收器的性能都有很明顯的提升,特別是穩(wěn)定版的ZGC垃圾回收器 2、不論任何機(jī)器配置下,都推薦使用ZGC,ZGC的停頓時間達(dá)到亞毫秒級,吞吐量也比較高
我在JDOS平臺上選擇了不同配置的機(jī)器(2C4G、4C8G、8C16G),并分別使用JDK8、JDK11和JDK17進(jìn)行部署和壓測。
整個壓測過程限時60分鐘,用180個虛擬用戶并發(fā)請求一個接口,每次接口請求都創(chuàng)建512Kb的數(shù)據(jù)。最終產(chǎn)出不同GC回收器的各項(xiàng)指標(biāo)數(shù)據(jù),來分析GC的性能提升效果。
以下是壓測的性能情況:
4、OracleJDK 和 OpenJDK 的選擇
2021年9月,Oracle宣布JDK17可以免費(fèi)商用,直到下一個 LTS 版本之后繼續(xù)提供整整一年,同時Oracle 將繼續(xù)按照自 Java 9 以來的相同版本和時間表提供GPL下的Oracle OpenJDK 版本。
2023年9月,OracleJDK發(fā)布了新的LTS版本 JDK21,這就意味著從2024年9月開始,在生產(chǎn)環(huán)境使用 OracleJDK17 將需要付費(fèi)。
參考: https://www.oracle.com/hk/java/technologies/downloads/#java17
?
OracleJDK和OpenJDK這兩個之間沒有真正的技術(shù)差別,因?yàn)獒槍racle JDK構(gòu)建過程是基于OpenJDK的。自從JDK11開始,OracleJDK和OpenJDK在功能上基本相同,所以推薦使用 OpenJDK17 或其他開源的JDK版本,這些開源版本都是基于OpenJDK構(gòu)建并提供長期支持的,比如:AdoptOpenJDK、RedHatOpenJDK。?
官方參考: https://blogs.oracle.com/java/post/oracle-jdk-releases-for-java-11-and-later
?
5、JDK11到JDK17帶來了哪些新特性
5.1、JVM改進(jìn)
1、ZGC垃圾回收器從實(shí)驗(yàn)性功能更改為正式產(chǎn)品功能,從JDK11引入以來,經(jīng)過持續(xù)的迭代升級,目前已經(jīng)足夠穩(wěn)定。需要手動開啟,開啟方式:-XX:+UseZGC
2、G1垃圾回收器仍然作為默認(rèn)垃圾回收器,進(jìn)行改進(jìn)升級,主要包括可中止的混合收集集合、NUMA 可識別內(nèi)存分配等
3、JDK14開始刪除 CMS 垃圾回收器
4、JDK14開始棄用 ParallelScavenge 和 SerialOld GC 的組合使用
5、JDK15禁用偏向鎖,默認(rèn)禁用:-XX:+UseBiasedLocking
6、NullPointerException 提示信息改進(jìn)
JDK14以前的出現(xiàn)NullPointerException時,只能定位到所在異常行,無法定位具體是哪個變量。改進(jìn)后的NullPointerException,可以清晰描述具體變量,提升了空指針異常的可讀性。
5.2、新語法特性
5.2.1、Switch表達(dá)式簡化
switch表達(dá)式帶來了簡化式的編碼方式,提供了新的分支切換方式,即 -> 符號,右則表達(dá)式方法體在執(zhí)行完分支方法之后,自動結(jié)束 switch 分支,同時 -> 右則方法塊中可以是表達(dá)式、代碼塊或者是手動拋出的異常
參考: https://openjdk.org/jeps/361
傳統(tǒng)寫法
新寫法
5.2.2、Text Blocks文本塊(5星推薦)
參考: https://openjdk.org/jeps/378
通過編寫 """,來減少轉(zhuǎn)義字符和換行符,達(dá)到簡化代碼和提高代碼可讀性的目的
5.2.3、Record類型
參考: https://openjdk.org/jeps/395
record 是 JDK 14 引入的關(guān)鍵字,用于聲明不可變的數(shù)據(jù)類。它適用于存儲純粹的值類型數(shù)據(jù),如接口傳輸數(shù)據(jù)、坐標(biāo)點(diǎn)和只讀的日志記錄。與 lombok 相比,record 簡化了定義純粹數(shù)據(jù)類型的過程。由于 record 類是不可變的,成員變量只能設(shè)置一次且無法更改,無需提供顯式的 setter() 方法。
1、定義Point類,使用關(guān)鍵字record,未定義get/set
2、查看編譯后的字節(jié)碼文件
3、使用Point類
5.2.4、instanceof 的模式匹配升級
?instanceof類型判斷再也不需要強(qiáng)制轉(zhuǎn)換
參考: https://openjdk.org/jeps/394
5.2.5、密封的類和接口
參考: https://openjdk.org/jeps/409
JDK15開始,引入了sealed普通類或接口類,這些類只允許被指定的類或者interface進(jìn)行擴(kuò)展和實(shí)現(xiàn)。
使用修飾符sealed,您可以將一個類聲明為密封類。密封的類使用關(guān)鍵字permits列出可以直接擴(kuò)展它的類。子類可以是最終的,非密封的或密封的
比較實(shí)用的一個特性,可以用來限制類的層次結(jié)構(gòu)
5.2.6、其他優(yōu)化和升級
感興趣的同學(xué),推薦閱讀OpenJDK官方文檔說明,從JDK11到JDK17的改動: https://openjdk.org/projects/jdk/17/jeps-since-jdk-11
?
6、升級步驟
6.1、JDK選擇
OpenJDK17下載:https://jdk.java.net/archive/
行云鏡像:jdt-base-tomcat/java-jdt-centos7.4-openjdk-17.0.2-tomcat8.0.53
6.2、pom編譯配置升級
maven編譯所需JDK升級至17
17/maven.compiler.source?> 17/maven.compiler.target?> /properties?>
6.3、SpringBoot升級
SpringBoot版本升級到2.7.15,Spring版本升級為5.3.29
為什么不升級到SpringBoot3?
Spring Boot 3.0最低要求 Java 17,SpringBoot3.0帶來了很多變化,和SpringBoot2差異較大。 考慮到公司很多中間件都是基于SpringBoot2構(gòu)建的,所以此處推薦升級到SpringBoot2的最高版本2.7.15。
POM升級
org.springframework.boot/groupId?> spring-boot-starter-parent/artifactId?> 2.7.15/version?> /parent?>
也可以通過設(shè)置dependencyManagement的方式:
!-- 框架版本配置--?> 2.7.15/springboot-version?> 5.3.29/springframework.version?> /properties?> org.springframework.boot/groupId?> spring-boot-starter-parent/artifactId?> ${springboot-version}/version?> import/scope?> pom/type?> /dependency?> org.springframework/groupId?> spring-framework-bom/artifactId?> ${springframework.version}/version?> import/scope?> pom/type?> /dependency?> /dependencies?> /dependencyManagement?>
參考:
spring升級指南: https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions
springboot版本官網(wǎng): https://spring.io/projects/spring-boot#learn
循環(huán)依賴問題
SpringBoot升級到2.7.15后,如果應(yīng)用中存在循環(huán)依賴的問題,啟動時會報如下錯誤:
原因:官方文檔不鼓勵循環(huán)依賴引用,默認(rèn)情況下是禁止的
解決方案:
第一種:推薦更新應(yīng)用中bean的依賴關(guān)系來解決
第二種:配置文件中加入以下配置,為了和舊版本保持一致,此配置推薦添加
#放開循環(huán)依賴 spring.main.allow-circular-references=true
6.4、常用中間件升級
6.4.1、Lombok版本升級到1.18.20以上
org.projectlombok/groupId?> lombok/artifactId?> 1.18.20/version?> /dependency?>
如果不升級,編譯時會報錯如下:
?
6.4.2、swgger問題,springfox3.0.0和springboot2.7版本不兼容
異常:
Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null
解決方案:
properties配置(感謝 zhaoyongping8 驗(yàn)證提供):
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
/** * 增加如下配置可解決Spring Boot 2.7.15 與Swagger 3.0.0 不兼容問題 **/ @Bean public BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { return new BeanPostProcessor() { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) { customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); } return bean; } private void customizeSpringfoxHandlerMappings(List mappings) { List copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null).collect(Collectors.toList()); mappings.clear(); mappings.addAll(copy); } @SuppressWarnings("unchecked") private List getHandlerMappings(Object bean) { try { Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings"); field.setAccessible(true); return (List) field.get(bean); } catch (IllegalArgumentException | IllegalAccessException e) { throw new IllegalStateException(e); } } }; }
參考:https://developer.aliyun.com/article/950787
6.4.3、AKS升級(針對直接從JDK8升級的情況)
異常:Causedby: java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
原因:Java11 刪除了 Java EE modules,其中就包括 java.xml.bind (JAXB)。
解決方案:
手動引入如下包即可
!-- API, java.xml.bind module --?> jakarta.xml.bind/groupId?> jakarta.xml.bind-api/artifactId?> 2.3.2/version?> /dependency?> !-- Runtime, com.sun.xml.bind module --?> org.glassfish.jaxb/groupId?> jaxb-runtime/artifactId?> 2.3.2/version?> /dependency?>
6.4.4、Concrete配置中心阻塞升級
新版本 2.2.1 已修復(fù)此問題
使用 Concrete時,啟動時異常:
Unable to make field private static final java.lang.reflect.Method jdk.proxy2.$Proxy97.m0 accessible: module jdk.proxy2 does not "opens jdk.proxy2" to unnamed module @61d47554
原因:
分析下Concrete報錯的原因,如下圖,包內(nèi)com.wangyin.concrete.spring.ConcreteConfigProcessor#postProcessAfterInitialization(212行)的實(shí)現(xiàn)邏輯
解決方案:
1、在JVM啟動參數(shù)中設(shè)置--add-opens jdk.proxy2來開啟私有字段的訪問,但因?yàn)閯討B(tài)代理生成的包名是隨機(jī)不明確的,所以這種方案不可行。JDK官方文檔也明確表示不支持訪問動態(tài)代理內(nèi)部的隨機(jī)字段。官方說明:https://cr.openjdk.org/~mr/jigsaw/spec/api/java/lang/reflect/Proxy.html
2、代碼修改,只需把 f.setAccessible(true) 移到 Modifier.isStatic(f.getModifiers()) 的判斷下方即可。原因是方法 Modifier.isStatic(f.getModifiers()) 本來就要跳過靜態(tài)字段,這樣修改直接避免了訪問。推動concrete團(tuán)隊(duì)修復(fù)問題
?
6.5、JVM啟動參數(shù)配置
6.5.1、開啟ZGC
啟動參數(shù)中配置:-XX:+UseZGC
6.5.2、不同中間件所需啟動參數(shù)
升級JDK17后,項(xiàng)目啟動時可能會遇到如下兩種類型的異常:
1、cannot access class sun.util.calendar.ZoneInfo (in module java.base) because module java.base does not export sun.util.calendar to unnamed module @0x2611f533
2、Unable to make field final int java.math.BigInteger.signum accessible: module java.base does not "opens java.math" to unnamed module @525f1e4e
異常原因:
自從JDK9中引入了模塊化功能后,再到JDK17,對于包掃描和反射的權(quán)限控制更加的嚴(yán)格。常見的庫比如(Spring)大量用到包掃描和反射,所以常出現(xiàn)此錯誤。
解決方案:
一個粗暴的解決辦法是將沒開放的module強(qiáng)制對外開放,即保持和Java9之前的版本一致。
?--add-exports導(dǎo)出包,意味著其中的所有公共類型和成員都可以在編譯和運(yùn)行時訪問。
?--add-opens打開包,意味著其中的所有類型和成員(不僅是公共類型)都可以在運(yùn)行時訪問。
主要區(qū)別在于--add-opens允許“深度反射”,即非公共成員的訪問,才可以調(diào)用setAccessible(true)
參考: https://stackoverflow.com/questions/44056405/whats-the-difference-between-add-exports-and-add-opens-in-java-9
?
SGM需要加入:
--add-opens java.management/java.lang.management=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED
R2M需要加入:
--add-opens java.base/java.time=ALL-UNNAMED
Ducc需要加入:
--add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED
AKS需要加入:
--add-exports java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED
6.6、啟動后的驗(yàn)證
1.推薦先升級JDK11,再到JDK17,一邊升級一邊進(jìn)行驗(yàn)證觀察
2.觀察日志是否有異常,特別是上面說到的啟動時異常
3.觀察監(jiān)控類軟件,比如SGM、UMP等監(jiān)控是否正常
4.推薦逐步有序切量,并做好常態(tài)化壓測,防止影響核心業(yè)務(wù)
5.升級完成后,最好能做個全流程的功能測試,防止功能異常
?
7、總結(jié)
1、升級后,除了可以使用新的語法特性,最大的亮點(diǎn)是可以使用亞毫秒級停頓的GC性能(至少百倍的GC性能提升),所以 強(qiáng)烈建議升級到JDK17
2、整個升級過程并不復(fù)雜,主要涉及到中間件版本的升級和啟動參數(shù)的配置
如果還停留在JDK8,推薦先升級JDK11,再到JDK17,具體升級步驟先參考我的上篇文章“JDK8升級JDK11最全實(shí)踐干貨來了”,再參考本章中的升級步驟。
希望以上分享可以給大家?guī)韺?shí)際的幫助,升級過程中如果遇到問題,歡迎大家在評論區(qū)回復(fù)。
?
系列文章:
?JDK8升級JDK11最全實(shí)踐干貨來了?
?JDK11升級JDK17最全實(shí)踐干貨來了?
?你還在“垃圾”調(diào)優(yōu)?快來看看JDK17的ZGC如何解放雙手?
?JDK17實(shí)踐升級經(jīng)驗(yàn)匯總?
審核編輯 黃宇
-
編譯
+關(guān)注
關(guān)注
0文章
657瀏覽量
32872 -
JDK
+關(guān)注
關(guān)注
0文章
81瀏覽量
16596
發(fā)布評論請先 登錄
相關(guān)推薦
評論