作者:京東零售 劉一達
前言
2006年之後SUN公司決定將JDK進行開源,從此成立了OpenJDK組織進行JDK程式碼管理。任何人都可以獲取該原始碼,並透過原始碼構建一個發行版釋出到網路上。但是需要一個組織稽核來確保構建的發行版是有效的, 這個組織就是JCP(Java Community Process)。2009年,SUN公司被Oracle公司"白嫖"(參考2018年Google賠款),此時大家使用的JDK通常都是Oracle公司的OpenJDK構建版本-OracleJDK。但是,Oracle公司是一個明顯只講商業而不管情懷的公司,接手Java商標之後,明顯加快了JDK的釋出版本。2018年9月25日,JDK11成功釋出,這是一個LTS版本,包含了17個JEP的更新。與此同時,Oracle把JDK11起以往的商業特性全部開源給OpenJDK(例如:ZGC和Flight Recorder)。根據Oracle的官方說法(Oracle JDK Releases for Java 11 and Later),從JDK11之後,OracleJDK與OpenJDK的功能基本一致。然後,Oracle宣佈以後將會同時發行兩款JDK:1. 一個是以GPLv2+CE協議下,由Oracle發行OpenJDK(簡稱為Oracle OpenJDK);2. 另一個是在OTN協議下的傳統OracleJDK。這兩個JDK共享絕大多數原始碼,核心差異在於前者可以免費在開發、測試和生產環境下使用,但是隻有半年時間的更新支援。後者各個人可以免費使用,但是生產環境中商用就必須付費,可以有三年時間的更新支援。
2021年9月14日,Oracle JDK17釋出,目前也是最新的Java LTS版本。有意思的是,Oracle竟然"朝令夕改",OracleJDK17竟然是免費的開源協議,並支撐長達8年的維護計劃。目前公司內部使用的OracleJDK8最高版本為1.8.0.192,而Oracle在JDK8上開源協議支援的最高免費版本為jdk1.8.0_202。2022年Spring6和SpringBoot3相繼推出,而支援的最低版本為JDK17。綜上所述,JDK8為目前絕大多數以穩定性為主的系統第一選擇,但是升級到高版本JDK也只是時間問題。下面圖表展示了JDk8到JDK17的每個版本升級的JEP個數。
透過以上圖表,我們可以得出結論,JDK8到JDK17包含大量新特性,為Oracle在Java近5年來的智慧結晶。目前市面上的公司還是隻有少數系統會選擇JDK11或者JDK17作為線上技術選型,如果選擇從JDK8升級到JDK17必然會有非常大的挑戰和較多需要填的坑。本文主要介紹JDK8到JDk17近200個JEP中比較有價值的新特性(按照價值從高到低排序),這裡有一部分特性作者也線上上環境使用過,也會將其中的使用心得分享給大家。
核心JEP功能及原理介紹
一、Java平臺模組化系統(Jigsaw專案)
JDK9最耀眼的新特性就是Java平臺模組化系統(JPMS,Java Platform Module System),透過Jigsaw專案實施。Jigsaw專案是Java發展過程的一個巨大里程碑,Java模組系統對Java系統產生非常深遠的影響。與JDK的函數語言程式設計和 Lamda表示式存在本質不同 ,Java模組系統是對整個Java生態系統做出的改變。
同時也是JDK7到JDK9的第一跳票王專案。Jigsaw專案本計劃於在2010年伴隨著JDK7釋出,隨著Sun公司的沒落及Oracle公司的接手,Jigsaw專案從JDK7一直跳票到JDK9才釋出。前後經歷了前後將近10年的時間。即使在2017JDK9釋出前夕,Jigsaw專案還是差點胎死腹中。原因是以IBM和Redhat為首的13家企業在JCP委員會上一手否決了Jigsaw專案作為Java模組化規範進入JDK9釋出範圍的規劃。原因無非就是IBM希望為自己的OSGI技術在Java模組化規範中爭取一席之地。但是Oracle公司沒有任何的退讓,不惜向JCP發去公開信,聲稱如果Jigsaw提案無法透過,那麼Oracle將直接自己開發帶有Jigsaw專案的java新版本。經歷了前後6次投票,最終JDK9還是帶著Jigsaw專案最終釋出了。但是,令人失望的是,Java模組化規範中還是給Maven、Gradle和OSGI等專案保留了一席之地。對於使用者來說,想要實現完整模組化專案,必須使用多個技術相互合作,還是增加了複雜性。如果大家想要對模組化技術有更多深入瞭解,推薦閱讀書籍《Java9模組化開發:核心原則與實踐》
1、什麼是Java模組化?
簡單理解,Java模組化就是將目前多個包(package)組成一個封裝體,這個封裝體有它的邏輯含義 ,同時也存在具體例項。同時模組遵循以下三個核心原則:
-
強封裝性:一個模組可以選擇性的對其他模組隱藏部分實現細節。
-
定義良好的介面:一個模組只有封裝是不夠的,還要透過對外暴露介面與其他模組互動。因此,暴露的介面必須有良好的定義。
-
顯示依賴:一個模組通常需要協同其他模組一起工作,該模組必須顯示的依賴其他模組 ,這些依賴關係同時也是模組定義的一部分。
2、為什麼要做模組化?
模組化是分而治之的一個重要實踐機制,微服務、OSGI和DDD都可以看到模組化思想的影子。現在很多大型的Java專案都是透過maven或者gradle進行版本管理和專案構建,模組的概念在Maven和gradle中早就存在,兩者的不同下文也會說到。現在讓我們一起回顧一下目前在使用JDK搭建複雜專案時遇到的一些問題:
2.1 如何使得Java SE應用程式更加輕量級的部署?
java包的本質只不過是類的限定名。jar包的本質就是將一組類組合到一起。一旦將多個Jar包放入ClassPath,最終得到只不過是一大堆檔案而已。如何維護這麼龐大的檔案結構?目前最有效的方式,也是隻能依賴mave或者gradle等專案構建工具。那最底層的Java平臺的Jar包如何維護?如果我只是想部署一個簡答的 helloworld應用,我需要一個JRE和一個使用者編譯的Jar包,並將這個Jar包放到classpath中去。JDK9以前,JRE的執行依賴我們的核心java類庫-rt.jar。rt.jar是一個開箱即用的全量java類庫,要麼不使用,要麼使用全部。直到JDK8,rt.jar的大小為60M,隨著JDK的持續發展,這個包必然會越來越大。而且全量的java類庫,給JRE也帶來了額外的效能損耗。Java應用程式如果能選擇性的載入rt.jar中的檔案該多好?
2.2 在暴露的JAR包中,如何隱藏部分API和型別?
在使用Dubbo等RPC框架中,provider需要提供呼叫的介面定義Jar包,在該Jar包中包含一個共該Jar包內部使用的常量聚合類Constannt,放在constant包內。如何才能暴露JAR包的同時,隱藏常量聚合類Constant?
2.3 一直遭受NoClassDefFoundError的折磨
透過什麼方式,可以知道一個Jar包依賴了哪些其他的 Jar包?JDK本身目前沒有提供,可以透過Maven工具完成。那為什麼不讓Java平臺自身就提供這些功能?
3、JPMS如何解決現有問題?
JPMS具有兩個重要的目標:
-
強封裝(Strong encapsulation): 每一個模組都可以宣告瞭哪些包是對外暴露的,java編譯和執行時就可以實施這些規則來確保外部模組無法使用內部型別。
-
可靠配置(Reliable configuration):每一模組都宣告瞭哪些是它所需的,那麼在執行時就可以檢查它所需的所有模組在應用啟動執行前是否都有。
Java平臺本身就是必須要進行模組化改造的複雜專案,透過Jigsaw專案落地。
3.1 Project Jigsaw
Modular development starts with a modular platform. —Alan Bateman 2016.9
模組化開始於模組化平臺 。Project Jigsaw 有如下幾個目標:
-
可伸縮平臺(Scalable platform):逐漸從一個龐大的執行時平臺到有有能力縮小到更小的計算機裝置。
-
安全性和可維護性(Security and maintainability):更好的組織了平臺程式碼使得更好維護。隱藏內部API和更明確的介面定義提升了平臺的安全性。
-
提升應用程式效能(Improved application performance):只有必須的執行時runtimes的更小的平臺可以帶來更快的效能。
-
更簡單的開發體驗Easier developer experience:模組系統與模組平臺的結合使得開發者更容易構建應用和庫。
對Java平臺進行模組化改造是一個巨大工程,JDK9之前,rt.jar是個巨大的Java執行時類庫,大概有60MB左右。JDK9將其拆分成90個模組左右 ,如下圖所示(圖片來源《Java 9模組化開發》):
4 建立第一個Java模組
建立一個Java模組其實非常的簡單。在目前Maven結構的專案下,只需要在java目錄下,新建一個module-info.java檔案即可。此時,當前目錄就變成了一個Java模組及Maven模組。
--moudule1
---src
----main
-----java
------com.company.package1
------moudule-info.java
---pom.xml
5 模組化對現有應用的影響
5.1 你可以不用但是不能不懂
Java模組化目前並沒有展現出其宣傳上的影響,同時也鮮有類庫正在做模組化的改造。甚至,本人在建立第一個模組的時候,就遇到了Lombook失效、深度反射失敗、Spring啟動失敗以及無法動態部署的影響。因此,儘量不要嘗試線上上環境使用模組化技術!不用,但是不代表你可以不懂!隨著Java平臺模組化的完成,執行在JDK9環境的Java程式就已經面臨著Jar包和模組的協作問題。未雨綢繆,在發現問題的時候,模組化技術可以幫你快速的定位問題並解決問題。
例如,在從JDK8升級到JDK11時,我們經常會收到一下警告:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.jd.jsf.java.util.GregorianCalendar_$$_Template_1798100948_0 (file:/home/export/App/deliveryorder.jd.com/WEB-INF/lib/jsf-1.7.2.jar) to field java.util.Calendar.fields
WARNING: Please consider reporting this to the maintainers of com.jd.jsf.java.util.GregorianCalendar_$$_Template_1798100948_0
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
透過反射訪問JDK模組內類的私有方法或屬性,且當前模組並未開放指定類用於反射訪問,就會出現以上告警。解決方式也必須使用模組化相關知識,可以使用遵循模組化之間的訪問規則,也可以透過設定 –add-opens java.base/java.lang = ALL-UNNNAMED 破壞模組的封裝性方式臨時解決;
5.2 Java模組、Maven模組和OSGI模組的之間的關係。
Java模組化技術,理論上可以從Java底層解決模組和模組之間的模組依賴、多版本、動態部署等問題。 前文所述,在2017JDK9釋出前夕,以IBM和Redhat為首的13家企業在JCP委員會上一手否決了Jigsaw專案作為Java模組化規範進入JDK9釋出範圍的規劃。經過眾多權衡,Java模組化規範中還是給Maven、Gradle和OSGI等專案保留了一席之地。目前,可以透過Java模組+Maven模組或者Java模組+OSGI模組的方式構建專案,可惜的是,使用多個技術相互合作,還是增加了複雜性。
5.3 模組化對類載入機制的影響
JDK9之後,首先取消了之前的擴充套件類載入器,這是清理之中,因為本身JRE擴充套件目錄都已經不存在,取而代之的是平臺類載入器。然後,類載入器的雙親委派模型機制進行了破壞,在子類將類委派給父類載入之前,會優先將當前類交給當前模組(Moudle)或層(Layer)的類載入器載入。所以會形成如下的類載入模型:
同時,在JDK9之後,引入的層(Layer)的概念,在Java程式啟動時,會解析當前模組路徑中的依賴關係,並形成一個依賴關係圖包含在引導層(Bootstrap Layer)中,這個依賴關係圖從此開始不再改變。因此,現在動態的新增模組要建立新的層,不同的層之間可以包含相同的模組。會形成如下所示的依賴關係(圖片來源《Java 9模組化開發》):
綜上所述,模組化改造對於使用自定義類載入器進行功能動態變化的程式還是巨大的,一旦使用模組化,必然會導致這類功能受到巨大影響。當然模組化技術普及還需要很長一段時間,會晚但是不會不來,提前掌握相關技術還是很必要。
5.4 總結
下面是Java模組化相關技術的一些核心腦圖,可以學習參考:
二、垃圾回收器的一系列最佳化措施
2.1、ZGC-新一代垃圾回收器
JDK11中,最耀眼的新特性就是ZGC垃圾回收器。作為實驗性功能,ZGC的特點包括:
-
GC停頓時間不會超過10ms。
-
停頓時間不會隨著堆的大小,或者活躍物件的大小而增加;
-
相對於G1垃圾回收器而言,吞吐量降低不超過15%;
-
支援Linux/x64、window和mac平臺;
-
支援8MB~16TB級別的堆回收。
同時根據openJDK官方的效能測試資料顯示(JEP333),ZGC的表現非常的出色:
-
在僅關注吞吐量指標下,ZGC超過了G1;
-
在最大延遲不超過某個設定值(10到100ms)下關注吞吐量,ZGC較G1效能更加突出。
-
在僅關注低延遲指標下,ZGC的效能高出G1將近兩個數量級。99.9th僅為G1的百分之一。
也正是因為如此,ZGC簡直是低延遲大記憶體服務的福音。話說如此,作者在嘗試使用ZGC過程中還是發現一些問題:
-
因為整個ZGC週期基本都是併發執行,因此建立新物件的速度與垃圾回收的速度從一開始就在較量。如果建立新物件的速度更勝一籌,垃圾會將堆佔滿導致部分執行緒阻塞,直到垃圾回收完畢。
-
G1雖然是第一個基於全域性的垃圾回收器,但是仍然存在新生代和老年代的概念。但是從ZGC開始,完全拋棄了新生代和老年代。但是新生代物件朝生夕滅的特性會給ZGC帶來很大的壓力。完全的併發執行,必然會造成一定的吞吐量降低。
-
在JDK11,G1垃圾回收器目前還只是實驗性的功能,只支援Linux/x64平臺。後續最佳化接改進,短時間內無法更新到JDK11中,所以可能會遇到一些不穩定因素。例如: 1. JDK12支援併發類解除安裝功能。2. JDK13將可回收記憶體從4TB支援到16TB。3. JDK14提升穩定性的同時,提高效能。4. JDK15從實驗特性轉變為可生產特性 。所以如果想要使用穩定的ZGC功能,只能升級到JDK17,橫跨一個JDK11LTS版本,同時面臨近200個JEP帶來的功能更新。
-
實際線上生產環境,在訂單商品等核心系統嘗試使用ZGC。但是壓測結果顯示,在JDK11還是JDK17都差強人意。當然這並不是代表ZGC本身技術缺陷,而是需要根據不同的線上環境做更深度的調優和實踐。因為資料保密等原因,這裡沒有給大家展示具體的壓測資料,讀者可以在各自環境進行不同程度的壓測驗證。
ZGC的原理介紹需要極大的篇幅,本文不打算對ZGC的底層技術展開大範圍討論。如果大家想要深入學習,作者推薦書籍《新一代垃圾回收器ZGC設計與實現》、Openjdk官網:ZGC介紹以及《深入理解Java虛擬機器第五版》中的一些介紹。
2.2、G1垃圾回收器相關
總的來講,得益於多個JEP最佳化,G1垃圾回收器無論是在JDK11還是JDK17都表現出了更強大的能力。隨著CMS垃圾回收器的廢棄,以及新生代ZGC的初出茅廬,G1垃圾回收器毫無疑問成了兼顧延遲和吞吐的最佳選擇。透過多次壓測結果觀察,只是簡單的提高JDK版本,就可以做到更低的GC時間、更短的GC間隔以及更少的CPU損耗。
場景 | JDK | 併發 | 基線參考 | TPS | TPM | TP99 | TP999 | TP9999 | MAX CPU |
---|---|---|---|---|---|---|---|---|---|
1.8.0_192 | 20 | -Xms12g -Xmx12g -XX:+UseG1GC -XX:ParallelGCThreads=13 -XX:ConcGCThreads=4 | 1680 | 97640 | 10 | 28 | 31 | 32 | 50.07% |
11.0.8 | 20 | -Xms12g -Xmx12g -XX:+UseG1GC -XX:ParallelGCThreads=13 -XX:ConcGCThreads=4 | 1714 | 99507 | 10 | 23 | 27 | 29 | 49.35% |
2.2.1、G1的Full GC從序列改為並行(JEP307)
G1垃圾回收器,在 Mix GC回收垃圾的速度小於新物件分配的速度時,會發生Full GC。之前,發生Full GC時採用的是Serial Old演算法,該演算法使用單執行緒標記-清除-壓縮演算法,垃圾回收吞吐量較高,但是Stop-The-World時間變長。JDK10,為了減少G1垃圾回收器在發生Full GC時對應用造成的影響,Full GC採用並行標記-清除-壓縮演算法。該演算法可以透過多執行緒協作 ,減少Stop-The-World時間。執行緒的數量可以由-XX:ParallelGCThreads選項來配置 ,但是這也會影響Young GC和Mixed GC執行緒數量。
2.2.2、可中斷的Mixed-GC(JEP344)
G1垃圾回收器,透過一種名為CSet的資料結構輔助實現可預測停頓模型演算法。CSet中儲存了GC過程中可進行垃圾回收的Region集合。在本特性之前,CSet一旦被確定,就必須全部掃描並執行回收操作,這可能會導致超過預期的垃圾回收暫停時間。因此,JEP344針對這種問題進行了最佳化。Java12 中將把 Cset拆分為強制及可選兩部分。有限執行強制部分的CSet,執行完成之後如果存在剩餘時間,則繼續處理可選Cset部分,從而讓GC暫停時間更接近預期值。
2.2.3 G1支援NUMA技術(JEP345)
非統一記憶體訪問架構(英語:non-uniform memory access,簡稱NUMA)是一種為多處理器的電腦設計的記憶體架構,記憶體訪問時間取決於記憶體相對於處理器的位置。在NUMA下,處理器訪問它自己的本地記憶體的速度比非本地記憶體(記憶體位於另一個處理器,或者是處理器之間共享的記憶體)快一些。ParallelGC在前幾年已經開始支援NUMA技術,並且對於垃圾回收器效能有較大提升。可惜的是,G1垃圾回收器在JDK14之前一直不支援此項技術,現在可以透過引數+XX:+UseNUMA在使用G1垃圾回收器時使用NUMA技術。
2.3、廢棄CMS垃圾回收器
CMS垃圾回收器在JDK9徹底被廢棄,在JDK12直接被刪除。目前,G1垃圾回收器是代替CMS的最優選擇之一。
2.4、廢棄ParallelScavenge + SerialOld 垃圾回收器組合
Java垃圾回收器有多種多樣的組合和使用方式。下面這張圖,我大概看過不差10遍,可是每次結果也是相同,記不住!!!!
預設垃圾回收器是哪些?
-XX:+UseParallelGC -XX:-UseParallelOldGC -XX:+UseParallelGC -XX:+UseParNewGC 這幾個引數有什麼區別?
CMS垃圾回收器有哪些關鍵引數?浮動垃圾怎麼處理?如何避免Full GC產生?
好訊息!這些以後都不用記憶了,我們只需要專注攻克三款垃圾回收器原理:預設大哥G1、新晉新星ZGC、非親兒子Shanondoah(瞭解)。這裡也許有人會抬槓,小記憶體CMS會有更好的表現。ParNew仍然是高吞吐服務的首選。大道至簡,簡單易用才是王道。G1和ZGC必定是以後JVM垃圾回收器的重點發展方向,與其耗費精力記憶即將淘汰的技術,不如利出一孔,精通一門!
2.4、Epsilon:低開銷垃圾回收器
Epsilon 垃圾回收器的目標是開發一個控制記憶體分配,但是不執行任何實際的垃圾回收工作。下面是該垃圾回收器的幾個使用場景:效能測試、記憶體壓力測試、極度短暫 job 任務、延遲改進、吞吐改進。
三、診斷和監控相關最佳化
3.1 Java Flight Recorder[JEP328]
Java Flight Recorder (JFR) 從正在執行的 Java 應用程式收集診斷和分析資料。 根據SPECjbb2015基準壓測結果顯示,JFR 對正在執行的 Java 應用程式的效能影響低於1%。 對於JFR的統計資料,可以使用 Java Mission Control (JMC) 和其他工具分析。 JFR 和 JMC 在 JDK 8 中是商業付費功能,而在 JDK11 中都是免費開源的。
3.2 Java Mission Control [JMS]
Java Mission Control (JMC) 可以分析並展示 Java Flight Recorder (JFR) 收集的資料,並且在 JDK 11 中是開源的。除了有關正在執行的應用程式的一般資訊外,JMC 還允許使用者深入瞭解資料。 JFR 和 JMC 可用於診斷執行時問題,例如記憶體洩漏、GC 開銷、熱點方法、執行緒瓶頸和阻塞 I/O。JMC可以作為現有JVM監控工具的一個補充,做到維度更多,監控更加實時(秒級),能從多個視角監控當前JVM程式的效能,更加更快速的定位並解決問題。
3.3 統一 JVM 日誌(JEP158)
在以往的低版本中很難知道導致JVM效能問題和導致JVM崩潰的根本原因。不同的JVM對日誌的使用是不同的機制和規則,這就使得JVM難以進行除錯。
解決這個問題最佳的方法:對所有的JVM元件引入一個統一的日誌框架,這些JVM元件支援細粒度的和易配置的JVM日誌。 JDK8以前常用的列印GC日誌方式:
-Xloggc:/export/Logs/gc.log //輸出GC日誌到指定檔案
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
3.3.1 目標:
-
所有日誌記錄的通用命令列選項。
-
透過tag對日誌進行分類,例如:compiler, gc, classload, metaspace, svc, jfr等。一條日誌可能會含有多個 tag
-
日誌包含多個日誌級別:error, warning, info, debug, trace, develop。
-
可以將日誌重定向到控制檯或者檔案。
-
error, warning級別的日誌重定向到標準錯誤stderr.
-
可以根據日誌大小或者檔案數對日誌檔案進行滾動。
-
一次只列印一行日誌,日誌之間無交叉。
-
日誌包含裝飾器,預設的裝飾器包括:uptime, level, tags,且裝飾可配置。
3.3.2 如何使用
-Xlog[:option]
option := [][:[][:[][:]]]
'help'
'disable'
what := [,...]
selector := [*][=]
tag-set := [+...]
'all'
tag := name of tag
level := trace
debug
info
warning
error
output := 'stderr'
'stdout'
[file=]
decorators := [,...]
'none'
decorator := time
uptime
timemillis
uptimemillis
timenanos
uptimenanos
pid
tid
level
tags
output-options := [,...]
output-option := filecount=
filesize=
parameter=value
-
可以透過配置-Xlog:help引數,獲取常用的JVM日誌配置方式。
-
可以透過-Xlog:disable引數關閉JVM日誌。
-
預設的JVM日誌配置如下:
-Xlog:all=warning:stderr:uptime,level,tags - 預設配置 - 'all' 即是包含所有tag - 預設日誌輸出級別warning,位置stderr - 包含uptime,level,tags三個裝飾
-
可以參考使用如下配置:
JDK9之前引數-XX:+PrintGCDetails可參考:
-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/export/Logs/gc-%t.log:time,tid,level,tags:filecount=5,filesize=50MB - safepoint表示列印使用者執行緒併發及暫停執行時間 - classhisto表示full gc時列印堆快照資訊 - age*,gc* 表示列印包括gc及其細分過程日誌,日誌級別info,檔案:/export/Logs/gc.log。 - 日誌格式包含裝飾符:time,tids,level,tags - default output of all messages at level 'warning' to 'stderr' will still be in effect - 儲存日誌個數5個,每個日誌50M大小
檢視GC前後堆、方法區可用容量變化,在JDK9之前,可以使用-XX::+PrintGeapAtGC,現在可參考:
-Xlog:gc+heap=debug:file=/export/Logs/gc.log:time,tids,level,tags:filecount=5,filesize=1M - 列印包括gc及其細分過程日誌,日誌級別info,檔案:/export/Logs/gc.log。 - 日誌格式包含裝飾符:time,tids,level,tags - default output of all messages at level 'warning' to 'stderr' will still be in effect - 儲存日誌個數5個,每個日誌1M大小
JDK9之前的GC日誌:
2014-12-10T11:13:09.597+0800: 66955.317: [GC concurrent-root-region-scan-start]
2014-12-10T11:13:09.597+0800: 66955.318: Total time for which application threads were stopped: 0.0655753 seconds
2014-12-10T11:13:09.610+0800: 66955.330: Application time: 0.0127071 seconds
2014-12-10T11:13:09.614+0800: 66955.335: Total time for which application threads were stopped: 0.0043882 seconds
2014-12-10T11:13:09.625+0800: 66955.346: [GC concurrent-root-region-scan-end, 0.0281351 secs]
2014-12-10T11:13:09.625+0800: 66955.346: [GC concurrent-mark-start]
2014-12-10T11:13:09.645+0800: 66955.365: Application time: 0.0306801 seconds
2014-12-10T11:13:09.651+0800: 66955.371: Total time for which application threads were stopped: 0.0061326 seconds
2014-12-10T11:13:10.212+0800: 66955.933: [GC concurrent-mark-end, 0.5871129 secs]
2014-12-10T11:13:10.212+0800: 66955.933: Application time: 0.5613792 seconds
2014-12-10T11:13:10.215+0800: 66955.935: [GC remark 66955.936: [GC ref-proc, 0.0235275 secs], 0.0320865 secs]
JDK9統一日誌框架輸出的日誌格式 :
[2021-02-09T21:12:50.870+0800][258][info][gc] Using G1
[2021-02-09T21:12:51.751+0800][365][info][gc] GC(0) Pause Young (Concurrent Start) (Metadata GC Threshold) 60M->5M(4096M) 7.689ms
[2021-02-09T21:12:51.751+0800][283][info][gc] GC(1) Concurrent Cycle
[2021-02-09T21:12:51.755+0800][365][info][gc] GC(1) Pause Remark 13M->13M(4096M) 0.959ms
[2021-02-09T21:12:51.756+0800][365][info][gc] GC(1) Pause Cleanup 13M->13M(4096M) 0.127ms
[2021-02-09T21:12:51.758+0800][283][info][gc] GC(1) Concurrent Cycle 7.208ms
[2021-02-09T21:12:53.232+0800][365][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 197M->15M(4096M) 17.975ms
[2021-02-09T21:12:53.952+0800][365][info][gc] GC(3) Pause Young (Concurrent Start) (GCLocker Initiated GC) 114M->17M(4096M) 15.383ms
[2021-02-09T21:12:53.952+0800][283][info][gc] GC(4) Concurrent Cycle
四、更加優雅的語法或者方法
4.1、集合工廠方法
List,Set 和 Map 介面中,新的靜態工廠方法可以建立不可變集合。
// 建立只有一個值的可讀list,底層不使用陣列
static <E> List<E> of(E e1) {
return new ImmutableCollections.List12<>(e1);
}
// 建立有多個值的可讀list,底層使用陣列
static <E> List<E> of(E e1, E e2, E e3) {
return new ImmutableCollections.List12<>(e1, e2,e3);
}
// 建立單例長度為0的Set結合
static <E> Set<E> of() {
return ImmutableCollections.emptySet();
}
static <E> Set<E> of(E e1) {
return new ImmutableCollections.Set12<>(e1);
}
4.2、介面私有方法
Java 8, 介面可以有預設方法。Java9之後,可以在介面內實現私有方法實現。
public interface HelloService {
public void sayHello();
// 預設方法
default void saySomething(){
syaEngHello();
sayHello();
};
// 私有方法
private void syaEngHello(){
System.out.println("Hello!");
}
}
4.3、改進的 Stream API
Java 9 為 Stream 新增了幾個方法:dropWhile、takeWhile、ofNullable,為 iterate 方法新增了一個過載方法。
// 迴圈直到第一個滿足條件後停止
default Stream takeWhile(Predicate predicate);
// 迴圈直到第一個滿足條件後開始
default Stream dropWhile(Predicate predicate);
// 根據表示式生成迭代器
static Stream iterate(T seed, Predicate hasNext, UnaryOperator next);
// 使用空值建立空的Stream,避免空指標
static Stream ofNullable(T t);
4.4、JShell
JShell 是 Java 9 新增的一個互動式的程式設計環境工具。它允許你無需使用類或者方法包裝來執行 Java 語句。它與 Python 的直譯器類似,可以直接 輸入表示式並檢視其執行結果。
4.5、區域性型別推斷(JEP286)
JDK10推出了區域性型別推斷功能,可以使用var作為區域性變數型別推斷識別符號,減少模板程式碼的生成 ,本質還是一顆語法糖。同時var關鍵字的用於與lombok提供的區域性型別推斷功能也基本相同。
public static void main(String[] args) throws Exception {
var lists = List.of("a", "b", "c");
for (var word : lists) {
System.out.println(word);
}
}
var關鍵字只能用於可推斷型別的程式碼位置,不能使用於方法形式引數,建構函式形式引數,方法返回型別等。識別符號var不是關鍵字,它是一個保留的型別名稱。這意味著var用作變數,方法名或則包名稱的程式碼不會受到影響。但var不能作為類或則介面的名字。
var關鍵字的使用確實可以減少很多沒必要的程式碼生成。但是,也存在自己的缺點:1. 現在很多IDE都存在自動程式碼生成的快捷方式,所以使不使用var關鍵字區別不大。2. 區域性型別推斷,不光是編譯器在編譯時期要推斷,後面維護程式碼的人也要推斷,會在一定程度上增加理解成本。
4.6、標準Java HTTP Client
使用過Python或者其他語言的HTTP訪問工具的人,都知道JDK提供的HttpURLConnection或者Apache提供的HttpClient有多麼的臃腫。簡單對比一下。
python 自帶的urllib工具:
response=urllib.request.urlopen('https://www.python.org') #請求站點獲得一個HTTPResponse物件
print(response.read().decode('utf-8')) #返回網頁內容
JDK:
HttpURLConnection connection = (HttpURLConnection) new URL("http://localhost:8080/demo/list?name=HTTP").openConnection();
connection.setRequestMethod("GET");
connection.connect();
int responseCode = connection.getResponseCode();
log.info("response code : {}", responseCode);
// read response
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} finally {
connection.disconnect();
}
Apache HttpClient:
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
// 建立Get請求
HttpGet httpGet = new HttpGet("http://localhost:12345/doGetControllerOne");
// 響應模型
CloseableHttpResponse response = null;
// 由客戶端執行(傳送)Get請求
response = httpClient.execute(httpGet);
// 從響應模型中獲取響應實體
HttpEntity responseEntity = response.getEntity();
System.out.println("響應狀態為:" + response.getStatusLine());
Java 9 中引入了標準Http Client API 。並在 Java 10 中進行了更新的。 到了Java11,在前兩個版本中進行孵化的同時,Http Client 幾乎被完全重寫,並且現在完全支援非同步非阻塞。與此同時它是 Java 在 Reactive-Stream 方面的第一個生產實踐,其中廣泛使用了 Java Flow API。
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://openjdk.java.net/"))
.build();
client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
4.7、Helpful NullPointerExceptions(JEP358)
隨著流式程式設計風格的流行,空指標異常成為了一種比較難定位的BUG。例如:
a.b.c.i = 99;
a[i][j][k] = 99;
在之前,我們只能收到以下異常堆疊資訊,然後必須藉助DEBUG工具調查問題:
Exception in thread "main" java.lang.NullPointerException
at Prog.main(Prog.java:5)
最佳化後,我們可以得到更加優雅的空指標異常提示資訊:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "c" because "a.b" is null
at Prog.main(Prog.java:5)
Exception in thread "main" java.lang.NullPointerException:
Cannot load from object array because "a[i][j]" is null
at Prog.main(Prog.java:5)
4.8、更加優雅的instance of 語法(JEP394)
以下程式碼是每個Java開發工程師的一塊心病:
if (obj instanceof String) {
String s = (String) obj; // grr...
...
}
上面的instanc of語法一共做了三件事:
-
判斷是否為String型別;
-
如果是,轉成String型別;
-
建立一個名為s的臨時變數;
在JDK16中,使用模式匹配思想改進了instance of 用法,可以做到以下最佳化效果:
if (obj instanceof String s) {// obj是否為String型別,如果是建立臨時變數s
// Let pattern matching do the work!
...
}
我們可以看到,整體程式碼風格確實優雅了很多。變數s的作用域為滿足條件的判斷條件範圍之內。因此,以下使用也是合法的:
if (obj instanceof String s && s.length() > 5) {// 因為&&具有短路功能
flag = s.contains("jdk");
}
但是以下用法,則會報錯:
if (obj instanceof String s || s.length() > 5) { // Error!
...
}
合理使用,則可以達到以下效果:
// 最佳化使用前
public final boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point other = (Point) o;
return x == other.x
&& y == other.y;
}
// 最佳化使用後:
public final boolean equals(Object o) {
return (o instanceof Point other)
&& x == other.x
&& y == other.y;
}
4.9、更加優雅的Switch用法
Java裡有一句名言:可以用switch結構實現的程式都可以使用if語句來實現。而且Swtich語法在某些工程師眼裡,根本沒有if語句簡潔。JDK14中提供了更加優雅的swtich語法,例如:
// 之前
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
// 之後
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> System.out.println(7);
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}
還可以把switch語句當成一個表示式來處理:
T result = switch (arg) {
case L1 -> e1;
case L2 -> e2;
default -> e3;
};
static void howMany(int k) {
System.out.println(
switch (k) {
case 1 -> "one";
case 2 -> "two";
default -> "many";
}
);
}
還可以配合關鍵字yield,在複雜處理場景裡,返回指定值:
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
yield result;
}
};
還有嗎?其實在JDK17中,還提出了Swtich 模式匹配的預覽功能,可以做到更優雅的條件判斷:
// 最佳化前
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
// 最佳化後
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
五、字串壓縮-Compact Strings(JEP254)
字串是我們日常程式設計中使用最頻繁的基本資料型別之一。目前,字串類底層都使用了一個字元陣列來實現,每個字元使用2個位元組(16位)空間。實際上,大量的字元都屬於Latin-1字元範圍內,我們只需要一個位元組就能儲存這些資料,因此這裡有巨大的可壓縮空間;SPECjbb2005壓測結果顯示對於GC時間及GC時間間隔都有一定程度的提升。詳細原理文件也可以參考【Oracle對CompackStrings分享】
六、 Java Flow API
Reactive Streams是一套非阻塞背壓的非同步資料流處理規範。從Java9開始,Java原生支援Reactive Streams程式設計規範。Java Flow API是對Reactive Streams程式設計規範的1比1復刻,同時意味著從Java9開始,JDK本身開始在Reactive Streams方向上進行逐步改造。
七、新一代JIT編譯器 Graal
即時編譯器在提高JVM效能上扮演著非常重要的角色。目前存在兩JIT編譯器:編譯速度較快但對編譯後的程式碼最佳化較低的C1編譯器;編譯速度較慢但編譯後的程式碼最佳化較高的C2編譯器。兩個編譯器在服務端程式及分層編譯演算法中扮演著非常重要的角色。但是,C2編譯器已經存在將近20年了,其中混亂的程式碼以及部分糟糕的架構使其難以維護。JDK10推出了新一代JIT編譯器Graal(JEP317)。Graal作為C2的繼任者出現,完全基於Java實現。Graal編譯器借鑑了C2編譯器優秀的思想同時,使用了新的架構。這讓Graal在效能上很快追平了C2,並且在某些特殊的場景下還有更優秀的表現。遺憾的是,Graal編譯器在JDK10中被引入,但是在JDK17(JEP410)中被廢除了,理由是開發者對其使用較少切維護成本太高。開發者也可以透過使用GraalVM來使用Graal編譯器;
總結
本文介紹了JDK9-JDK17升級過的近200個JEP中作者狹隘角度認為價值較高的功能做了一個綜述類介紹。主要目的有兩個:
-
透過本文,大家可以對即將使用的JDK11及JDK17新特性有一個籠統的瞭解,希望可以看到一些Java預發最近幾年的發展方向。
-
透過本文也可以看出,從JDK9到JDK17,Java生態還是生機勃勃。大量功能的更新意味著更優秀的效能及更高效的開發效率,積極主動的嘗試高版本JDK;
當然,JDK8到JDK17還有需求優秀的新特性,例如:shanondoah垃圾回收器、Sealed Classes、Records;
鑑於本人能力有限,文中會出現一些漏洞,希望大家找出並指正,讓本文成長為後續JDK17升級的掃盲手冊;