前言
雖然這篇文章的標題寫的是正確看待Java以及何時應該升級到JDK 17,但是實際上可以認爲是我對技術選型和系統性軟件工程的一些總結,其中包含了一些可以用於其它技術的參考性討論。做了很多年的Java之後,這幾年筆者在做lightdb數據庫內核開發中以c/c++爲主,所以可維護性和是否有顯而易見的收益會成爲我評估的很重要的因素。我會盡可能客觀的描述從運行時和可維護性角度(我並不是說其它維度不重要,只是相比來說,to B系統這兩方面會更重要),如何去評測或者客觀的評價一個選型或者使用某個特性是否划算,而且我希望這個結論是用戶、ISV、開發人員三方都贏。雖然以JDK什麼時候應該升級爲例,我希望它能在一些評估選型或升級時參考。
對於Java作爲企業級開發語言,我認爲應該首先區分抖動高度敏感系統(包括交易系統、數據庫)和其它系統看待Java的適用性,因爲現在的系統都很龐大,所以應該成爲那個應用場景而非系統更合適,這裏就不區分,大家能理解即可。從軟件工程維度來說,這分爲運行時成效和長期開發成效兩方面。
l 運行時成效:分吞吐量和抖動容忍性。就單請求數據量不是很大的事務比如MB級別來說,吞吐量上java不存在明顯的問題,即使單實例不滿足要求,完全可以通過分佈式達到線性擴展能力。從時延[張君華1] 方面來看,因爲不管是minor gc(它也會stop the world,但因爲大部分應用的大部分對象在新生代的時候幾乎都會被回收,它會快到微秒級,以至於絕大部分應用基本無感知)還是majar gc、full gc,或多或少的都會stop the world,由於任何時候,一個服務中都可能引用對象、新執行某個需要創建很多對象的操作,都可能導致gc來帶不確定的停頓,所以在抖動容忍性上,java是不太能打包票預估會不會發生的,我們能做的是儘可能的讓它足夠低(比如每次都控制在1毫秒內,通過24小時的持續壓測等)。這個問題和採用異步複製的高可用架構是一樣的,好像概率很低,但是出現災備切換時,丟數據或不正確的概率很高。在Java中,該問題一樣可以驗證,緩存在JVM堆中的數據[張君華2] 越多,gc時間[張君華3] 會越長,即使它是不必要的(當然,我們可以通過堆外解決,這也是條路,它僅僅是爲了減少不必要的GC,仍然比c/c++容易得多,比redis靠譜得多)。
l 長期開發成效:爲什麼有個修飾語長期呢?不同於to C系統,對於to B系統來說,沒有上生產還好說,一旦上了生產,下線那是極爲複雜的事情,不是一句簡單的跟客戶說系統我們打算不要了,限定你在某年某月末日之前把數據遷移走,這會導致用戶很無奈、而你沒有生意可做。具體展開包括幾個方面:一、從應用開發角度來說,java生態對於企業應用開發提供了現成的基建和可複用的各種庫,涵蓋從數據庫、中間件、文件操作到圖片、PDF、大數據、批處理等,大大降低了入門開發門檻和開發速度。但是反過來,這使得庫出現什麼問題的話,排查和解決可能會比較耗時,spring全家桶自己的大基建以及在各個庫的基礎上又增加了二次封裝使得排查更爲複雜。其二,開發人員很關心時髦的新技術、新框架、新的工具庫(所謂的面向面試編程,一個系統完成同一類功能的庫找出幾個一點不奇怪,比如json、xml、連接池、字節碼),很多庫在系統開發完成後通常已經升級了多個版本或者已經不在主流,舊客戶端不兼容新服務端的情況,三方庫版本之間的依賴兼容問題也會引起一連串版本的更新,也存在其他開發通常不夠熟悉或者壓根沒有使用過的情況,這都會導致不精細化管理三方庫和版本(注意:這不是說建一個系統維護依賴關係、定期檢查一遍就高枕無憂,這是一個內容治理的問題,參考運營有些文章PV可以幾十萬,有些一直在幾百),長期開發成效不達預期甚至看起來做無用功的情況。第三,關於安全,Java三方的安全漏洞是出了名的多,現代用戶很關心安全性,雖然很多部署在內網的系統CVE漏洞其實壓根不會發生,因爲多層防火牆、應用安全已經保障的完全足夠,但我仍然把它歸類爲中性看待,在技術棧升級成本與收益中我會講述我的理由。
這兩方面比較難以評估在於,通常在系統開發完成上線後至少一兩年後或原始開發不在之後纔會顯現出來,然後你發現當初真應該規範下。其次,我們通常在沒有異常之前不願意承認別人指出自己的方案存在瑕疵(注:這兩年我被別人指出不合理後,主動去調整特別多,包括功能完善、架構上、規範上,不乏一些我們需要兩個RP甚至三個RP才能切換到位甚至較大返工的)。
spring對JDK版本支持的滾動週期
講完了上下文,現在切入正文,討論升級到JDK 17是否是一件值得考慮的事。每出一個大版本,網上關於爲什麼應該升級到新版本的JDK都會有較多的文章,一般都是總結性或羅列各種新特性,至於那些特性對用戶來說是否長期適用,分析的會比較少。從事後來看,你會發現很多JDK 17甚至更早時候廠商、諮詢公司的一些預測其實噱頭成分更多一些,只是如果不跟隨好像顯得不夠入流,舉個典型的例子如AOT(AOT 編譯:GraalVM 可以將 Java 程序靜態編譯成本地機器碼,這被稱爲 Ahead-of-Time(AOT)編譯。AOT 編譯可以提供更快的啓動時間和更低的內存消耗,適用於一些對性能要求較高的場景),它在理論上不太行得通,因爲java使用c/c++寫的,而C++一致都未能很好的支持反射機制,所以註定了AOT和native image的靈活性很受限,用於需要廣泛靈活可變的框架和企業級開發更是根基不夠成熟。所以,分析哪些特性已經真正能帶來本質性改進,它是否帶來的收益高於成本,以及如何度量反而是升級前更重要的事。
java之所以如此流行,核心生態spring功不可沒,Spring對JAVA有風向標作用,所以spring對jdk版本的態度會很大的影響整個Java界對JDK版本的態度,因爲新啓動和開發的項目、新入行的開發人員可能不會關心使用jdk 8還是jdk 11或更新版本,但它一定會使用最新主版本的spring框架,因爲官方的腳手架和入門示例默認就是最新主推的版本。自JDK 17在2021年發佈後,2022年發佈的SpringBoot3(Spring6)直接限制最低爲JDK17,可預見:爲了使用Spring最新框架,很多團隊和開發者將被迫從Java 11(甚至Java 8)直接升級到Java 17版本。在此之前,Java社區一直是"新版任你發,我用Java 8",不管新版本怎麼出,很少有人願意升級,包括2018年發佈的JDK 11 LTS[張君華4] 。
那爲什麼spring 6開始從JDK 17開始直接支持呢?一方面,大多數框架,不管是開源還是商業中間件喜歡做宏大而優雅的事情,所以Spring的開發者會覺得,我都用Java 17的新API寫代碼,簡化邏輯,使用新特性,很棒;但是爲了支持Java8,需要寫很多workaround,那就很不爽,很不優雅。
但是站在工程的角度,作爲Spring框架的使用者,自然是希望Spring框架能夠支持更多的Java版本,更多的用戶。最好不用改代碼,最好不用升級Java版本,最好啥都不用改就能用。
本質上來說,這個反映了框架維護者和框架使用者的矛盾。所以它不得不定期滾動往前,歷史上spring也是這麼做的。如下所示:
Spring[張君華5] Version |
Java Version |
6.x |
JDK 17+ |
5.x |
JDK 8, 9, 10, 11, 12, 13, 17,18,19 Spring Framework 5.3.x:JDK 8-19 |
4.3.x |
JDK 6, 7, 8 |
4.2.x |
JDK 6, 7, 8 |
4.1.x |
JDK 6, 7, 8 |
4.0.x |
JDK 6, 7, 8 |
3.2.x |
JDK 6, 7 |
3.1.x |
JDK 5, 6, 7 |
3.0.x |
JDK 5, 6 |
spring 5對JDK的最低版本要求是JDK 8,看起來spring 6要求最低JDK 17,並不是非常規操作。而JDK 11更像JDK 7是個陪跑的版本。
注:說句題外話,實際上linux對glibc也是差不多的,只不過大部分用戶感知不明顯而已(我們很關心到底用gcc 4.8、gcc 7.3還是clang,以及centos和kylin的差異,所以還是挺明顯)。
如此看起來,未來兩三年升級到JDK 17或21是不得不需要納入日程考慮的事,只不過兼容性和過渡會花費不少的成本,如果系統還沒有完全穩定,則更需要謹慎評估,這涉及照顧各個方面,這裏就不展開。
那爲什麼Java8佔比這麼多
Java8提供了很多特性,比如Lambda 表達式、Optional 類,相當不錯的性能和穩定性,加上Java8超長的支持時間(令很多人意外的是,Java11支持的時間到2026年9月,Java8的支持時間到2030年12月,從這個層面上來說Java8比Java11甚至JDK 17[張君華6] 都要“長壽”),都導致Java8完全足夠使用。
這導致Java 8之後的版本,現在看來吸引力不是很大,模塊化有break changes,異步接口有netty,AOT/GraalVM在後端也不必要、甚至被廢棄。唯一比較期待的Project loom還遙遙無期,只能拿wisp2湊合用。
Java 8之後的分發策略,支持方式的變更,也會讓開發者在升級前要仔細考慮。比如Java11你用哪個發行版?Oracle會有潛在法律問題(雖然Oracle在JDK 17發佈的時候聲明後續JDK全部免費使用,最近又在查許可,所以很難讓用戶放心);AdoptOpenJDK的支持能不能跟上等等。
JDK9以來累計的主要新增特徵
現在來看一下近10年JDK引入的主要特性是否值得讓我們爲此升級。同樣,主要從長期開發成效和運行時效果兩方面來看。
1.局部變量類型推斷
這是自 Java 8 以來添加到 Java 中的最受開發者歡迎的功能之一,和C++ 11的auto自動類型推導類似。它允許你在不指定類型的情況下聲明局部變量。類型是從表達式的右側推斷出來的。此功能也稱爲var類型。
在上面的示例中,兩個程序將生成相同的輸出,但在 Java 10 的情況下,我們使用而var不是指定類型。說實話,這個特性很容易被誤用,尤其是Java的類型名通常非常長,導致我們都不願意寫,很可能就會大面積使用,但調用層次深了之後,可能排查效率會降低。
5.模式匹配instanceof
模式匹配instanceof是 Java 16 中添加的一項新功能。它允許你將instanceof運算符用作返回已轉換對象的表達式。當你使用嵌套的 if-else 語句時,這非常有用。在下面的示例中,你可以看到我們如何使用instanceof運算符來捕獲Employee對象,而不是進行顯式轉換。
相比類型推導,instanceof通常之後對象都會強轉,而且使用的點作用範圍比較受控,所以算是個還不錯的語法糖。
2.switch表達式
在 Java 14 中使用 switch 表達式時,你不必使用關鍵字break來跳出 switch 語句或return在每個 switch case 上使用關鍵字來返回值;相反,你可以返回整個 switch 表達式。這種增強的 switch 表達式使整體代碼看起來更清晰,更易於閱讀。更重要的是,能避免不經意的遺漏導致邏輯不正確,以往只能依賴代碼檢測工具,還是能夠改進質量的。
3.文本塊
文本塊是 Java 15 中添加的一項新功能。它允許你在不使用轉義序列的情況下創建多行字符串。這在你創建 SQL 查詢或 JSON 字符串時非常有用。在下面的示例中,你可以看到使用文本塊時代碼看起來更加簡潔。
還是能提升更多的易讀性,原來建議放在配置文件中的一些文本片段,如JSON、XML、SQL,可以直接硬編碼在java源文件中,還是有幫助的。
4.Records
記錄Records是添加到 Java 14 的一項新功能。它允許你創建用於存儲數據的類。它類似於 POJO 類,但代碼少得多;大多數開發人員使用 Lombok 生成 POJO 類,但是有了記錄,你就不需要使用任何第三方庫。在下面的示例中,你可以看到創建記錄類所需的代碼非常少。
雖然看起來不明顯,但是這個語法糖是相當實用,因爲lombok[張君華7] 並不是完全和pojo以及mybatis的慣例一致,還有漏洞[張君華8] 。有了record之後,就可以不用引入三方的lombok了。
6. 密封類
密封類是添加到 Java 17 中的一項新功能。它允許你將類或接口的繼承限制爲一組有限的子類。當你想將類或接口的繼承限制爲一組有限的子類時,這非常有用。在下面的示例中,你可以看到我們如何使用sealed關鍵字將類的繼承限制爲一組有限的子類。
密封類的子類可以聲明爲final或non-sealed。final 子類不能進一步擴展,而非密封子類可以進一步擴展。
對於框架開發和公共模塊的演進來說,該特性特別有用。尤其是某些子類我們從A系統複用到B系統,或者X和Y系統整合爲Z系統的時候,或者一些開發規範做了調整,從而創建了新的子類,但是又不希望老的子類使用從而造成無法收編。
7. 有用的 NullPointerException
NullPointerExceptions 是 Java 14 中添加的一項新功能。它允許你獲取有關NullPointerExceptions. 這在調試時非常有用NullPointerExceptions。在下面的示例中,你可以看到相同的代碼如何NullPointerExceptions在 Java 8 和 Java 14 中生成不同的結果,但在 Java 14 中,你可以獲得有關異常的更多信息
從問題排查的角度來看,對於異常的任何改進都是有百利而無一弊。
8. 性能提升[張君華9]
雖然說性能和GC停頓被我放在最後,但他倆其實是之前我以java爲主期間最關心的特性,而且我覺得它倆就是最重要的。先說三方數據,從規劃調度引擎 OptaPlanner 項目對 JDK8、JDK 17和 JDK 11 的性能基準測試進行了對比來看:
- 對於 G1GC(默認),Java 17 比 Java 11 快 8.66%;
- 對於 ParallelGC,Java 17 比 Java 11 快 6.54%;
- Parallel GC 整體比 G1 GC 快 16.39%;
簡而言之,JDK17 更快,高吞吐量垃圾回收器比低延遲垃圾回收器更快,G1表現最差(JDK 17默認GC)。從我實際跑renaissance-benchmarks基準測試(覆蓋web、計算密集型、併發密集型)來看,總體來說JDK 17明顯好於JDK 8,普遍在0%-20%之間,有幾個特例是scala-doku和page-rank,相差高達250%。
9. 低時延停頓的ZGC[張君華10]
ZGC 是 Java 11 中引入的最爲矚目的垃圾回收特性,是一種可伸縮、低延遲的垃圾收集器,不過在 Java 11 中是實驗性的引入,主要用來改善 GC 停頓時間,並支持幾百 MB 至幾個 TB 級別大小的堆,並且應用吞吐能力下降不會超過 15%,目前只支持 Linux/x64 位平臺的這樣一種新型垃圾收集器。
Java 13 中對 ZGC 的改進,把之前的限制基本都解決了,包括:
- 釋放未使用內存給操作系統。默認情況下是開啓的,不過可以使用參數:-XX:-ZUncommit 顯式關閉,同時如果將最小堆大小 (-Xms) 配置爲等於最大堆大小 (-Xmx),則將隱式禁用此功能。
- 支持最大堆大小爲 16TB
- 添加參數:-XX:SoftMaxHeapSize 來軟限制堆大小
具體gc停頓方面,根據openJDK官方的性能測試數據顯示(JEP333,基於SPECjbb 2015),ZGC的表現相當不錯:
- 在僅關注吞吐量指標下,ZGC超過了G1;
- 在最大延遲不超過某個設定值(10到100ms)下關注吞吐量,ZGC較G1性能更加突出。
- 在僅關注GC低延遲指標下,ZGC的性能高出G1很多。99.9th僅爲G1的百分之一。zgc中沒有分代的概念,所以,jstat中CGC/YGC並不體現ZGC的垃圾回收,需要通過-Xlog:gc進行分析。
由於SPECjbb 2015是一個收費工具,沒有提供開源版本,故我們使用renaissance-benchmark進行測試(其定位和SPECjbb 2015類似,也用於評估JVM的JIT、GC、解釋器等),在設置-XX:MaxGCPauseMillis的情況下,ZGC具有比較明顯的優勢。因爲使用Java的應用大部分都非時延高度敏感的系統,所以對ZGC的量化價值和競爭力其實並不那麼直接,但金融行業交易類業務系統和做基礎框架、中間件的社區通常會足夠關注和重視。
整體來說,ZGC對低延遲大內存服務確實有着明顯的好處。
從主要的新特性來看,確實其實並不那麼的明顯,所以大家不積極並不奇怪,也說明JDK 8絕大部分場景都夠用。從理性來看,需要整體完整的驗證下是否一些密集型、高吞吐系統會帶來明顯的用戶所需的提升,比如抖動下降、時延下降、吞吐量提升。
技術棧升級成本與收益
這事從不同干係人的角度來看,都是有利有弊,而且夾雜着不得不做的剛需消費。總體來說利大於弊,因爲安全漏洞、版權重視等其實是利好,雖然看起來會增加直接開發、維護成本,但是行業會更加健康、良性發展。其次,技術棧大版本升級通常都會從底層提升性能和易用性,利用新的硬件和內核特性、放棄過老的硬件支持的冗餘代碼,比糾結方法調用、內聯函數、協議細微差別等帶來更加明顯的系統性全局性能提升,對於關心性能和使用了新硬件的用戶,它有直接性收益體現。
關於技術棧升級,有一個規律:越貼近底層,越會有標準化的抽象接口,獨立升級會越容易。比如linux kernel升級,只需要升級下,驗證下沒有問題就可以了(最多改一些應用配置)。但是 Java 版本升級,就需要仔細檢查各個depdency的依賴,然後還要做大量的業務測試才能升級。如果是Spring升級,就會更加麻煩,需要改代碼,需要測試驗證;甚至會發現舊的使用方式不能用,需要修改核心邏輯等。在這種情況下,Spring要新推出一個大版本,必然需要一些殺手特性纔可以讓使用者升級。要不然就只能萬年Spring 5.x了,Spring社區自然也不希望這種事情發生。
總結和展望
Spring 6出來後,會有新公司、新項目使用,但是對於舊的項目,會依然在舊的版本上繼續運行。
目前來說國內很多程序猿可能覺得升級會造成額外工作,出了問題費力不討好,要是出了安全問題,更是頭疼。也有說沒有實質性的好處,而且還有風險,還有從企業角度說,未來也不升級,因爲去Oracle化。但考慮到未來oracle不再維護JDK8,Spring也不再維護過去版本的時候,爲了跟上時代,使用最新技術,必然會助推JDK的升級。
當越來越多的公司加入到JDK17以上的大軍中,未來更多的框架新版本都會最低支持JDK17,因爲兼容舊JDK實在不值得,當大部分框架和社區、論壇都是討論JDK17的技術和各種解決問題的方法時,必然會反推企業進行升級。
[張君華9]JDK不同版本性能對比,基本和實際測試有差距,但通常JDK 17更快