Java 12 新特性概述
Java 12 已如期於 2019年3 月 19 日正式發布,此次更新是 Java 11 這一長期支持版本發布之后的一次常規更新,截至目前,Java 半年為發布周期,並且不會跳票承諾的發布模式,已經成功運行一年多了。通過這樣的方式,Java 開發團隊能夠將一些重要特性盡早的合並到 Java Release 版本中,以便快速得到開發者的反饋,避免出現類似 Java 9 發布時的兩次延期的情況。
Java 12 早在 2018 年 12 月便進入了 Rampdown Phase One 階段,這意味着該版本所有新的功能特性被凍結,不會再加入更多的 JEP。該階段將持續大概一個月,主要修復 P1-P3 級錯誤。主要時間節點如下:
- 2018-12-13 Rampdown 第一階段 ( 從主線分離 )
- 2019-01-17 Rampdown 第二階段
- 2019-02-07 發布候選階段
- 2019-03-19 正式發布
本文主要針對 Java 12 中的新特性展開介紹,讓您快速了解 Java 12 帶來的變化。
Shenandoah:一個低停頓垃圾收集器(實驗階段)
Java 12 中引入一個新的垃圾收集器:Shenandoah,它是作為一中低停頓時間的垃圾收集器而引入到 Java 12 中的,其工作原理是通過與 Java 應用程序中的執行線程同時運行,用以執行其垃圾收集、內存回收任務,通過這種運行方式,給虛擬機帶來短暫的停頓時間。
Shenandoah 垃圾回收器是 Red Hat 在 2014 年宣布進行的一項垃圾收集器研究項目,旨在針對 JVM 上的內存收回實現低停頓的需求。該設計將與應用程序線程並發,通過交換 CPU 並發周期和空間以改善停頓時間,使得垃圾回收器執行線程能夠在 Java 線程運行時進行堆壓縮,並且標記和整理能夠同時進行,因此避免了在大多數 JVM 垃圾收集器中所遇到的問題。
據 Red Hat 研發 Shenandoah 團隊對外宣稱,Shenandoah 垃圾回收器的暫停時間與堆大小無關,這意味着無論將堆設置為 200 MB 還是 200 GB,都將擁有一致的系統暫停時間,不過實際使用性能將取決於實際工作堆的大小和工作負載。
圖 1. Shenandoah GC 工作周期如下所示
本圖片引用自:Shenandoah GC
上圖對應工作周期如下:
- Init Mark 啟動並發標記 階段
- 並發標記遍歷堆階段
- 並發標記完成階段
- 並發整理回收無活動區域階段
- 並發 Evacuation 整理內存區域階段
- Init Update Refs 更新引用初始化 階段
- 並發更新引用階段
- Final Update Refs 完成引用更新階段
- 並發回收無引用區域階段
需要了解不是唯有 GC 停頓可能導致常規應用程序響應時間比較長。具有較長的 GC 停頓時間會導致系統響應慢的問題,但響應時間慢並非一定是 GC 停頓時間長導致的,隊列延遲、網絡延遲、其他依賴服務延遲和操作提供調度程序抖動等都可能導致響應變慢。使用 Shenandoah 時需要全面了解系統運行情況,綜合分析系統響應時間。各種 GC 工作負載對比如下所示:
圖 2. 各種 GC 工作負載對比
本圖片引用自:Shenandoah GC
下面推薦幾個配置或調試 Shenandoah 的 JVM 參數:
-XX:+AlwaysPreTouch
:使用所有可用的內存分頁,減少系統運行停頓,為避免運行時性能損失。-Xmx == -Xmsv
:設置初始堆大小與最大值一致,可以減輕伸縮堆大小帶來的壓力,與AlwaysPreTouch
參數配合使用,在啟動時提交所有內存,避免在最終使用中出現系統停頓。-XX:+ UseTransparentHugePages
:能夠大大提高大堆的性能,同時建議在 Linux 上使用時將/sys/kernel/mm/transparent_hugepage/enabled
和/sys/kernel/mm/transparent_hugepage/defragv
設置為:madvise
,同時與AlwaysPreTouch
一起使用時,init
和shutdownv
速度會更快,因為它將使用更大的頁面進行預處理。-XX:+UseNUMA
:雖然Shenandoah
尚未明確支持 NUMA(Non-Uniform Memory Access),但最好啟用此功能以在多插槽主機上啟用 NUMA 交錯。與AlwaysPreTouch
相結合,它提供了比默認配置更好的性能。-XX:+DisableExplicitGC
:忽略代碼中的System.gc()
調用。當用戶在代碼中調用System.gc()
時會強制 Shenandoah 執行 STW Full GC ,應禁用它以防止執行此操作,另外還可以使用-XX:+ExplicitGCInvokesConcurrent
,在 調用System.gc()
時執行 CMS GC 而不是 Full GC,建議在有System.gc()
調用的情況下使用。
不過目前 Shenandoah 垃圾回收器還被標記為實驗項目,需要使用參數:- XX:+UnlockExperimentalVMOptions
啟用。更多有關如何配置、調試 Shenandoah 的信息,請參閱 henandoah wiki。
增加一套微基准套件
Java 12 中添加一套新的基本的微基准測試套件,該套微基准測試套件基於 JMH(Java Microbenchmark Harness),使開發人員可以輕松運行現有的微基准測試並創建新的基准測試,其目標在於提供一個穩定且優化過的基准,其中包括將近 100 個基准測試的初始集合,並且能夠輕松添加新基准、更新基准測試和提高查找已有基准測試的便利性。
微基准套件與 JDK 源代碼位於同一個目錄中,並且在構建后將生成單個 Jar 文件。但它是一個單獨的項目,在支持構建期間不會執行,以方便開發人員和其他對構建微基准套件不感興趣的人在構建時花費比較少的構建時間。
要構建微基准套件,用戶需要運行命令:make build-microbenchmark
,類似的命令還有:make test TEST="micro:java.lang.invoke"
將使用默認設置運行 java.lang.invoke
相關的微基准測試。關於配置本地環境可以參照文檔 docs/testing.md|html
。
Switch 表達式擴展(預覽功能)
Java 11 以及之前 Java 版本中的 Switch 語句是按照類似 C、C++ 這樣的語言來設計的,在默認情況下支持 fall-through
語法。雖然這種傳統的控制流通常用於編寫低級代碼,但 Switch 控制語句通常運用在高級別語言環境下的,因此其容易出錯性掩蓋其靈活性。
在 Java 12 中重新拓展了 Switch 讓它具備了新的能力,通過擴展現有的 Switch 語句,可將其作為增強版的 Switch 語句或稱為 "Switch 表達式"來寫出更加簡化的代碼。
Switch 表達式也是作為預覽語言功能的第一個語言改動被引入新版 Java 中來的,預覽語言功能的想法是在 2018 年初被引入 Java 中的,本質上講,這是一種引入新特性的測試版的方法。通過這種方式,能夠根據用戶反饋進行升級、更改,在極端情況下,如果沒有被很好的接納,則可以完全刪除該功能。預覽功能的關鍵在於它們沒有被包含在 Java SE 規范中。
在 Java 11 以及之前版本中傳統形式的 Switch 語句寫法如下:
清單 1. Switch 語句示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
int dayNumber;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
dayNumber = 6;
break;
case TUESDAY:
dayNumber = 7;
break;
case THURSDAY:
case SATURDAY:
dayNumber = 8;
break;
case WEDNESDAY:
dayNumber = 9;
break;
default:
throw new IllegalStateException("Huh? " + day);
}
|
上面代碼中多處出現 break 語句,顯得代碼比較冗余,同時如果某處漏寫一段 break 語句,將導致程序一直向下穿透執行的邏輯錯誤,出現異常結果,同時這種寫法比較繁瑣,也容易出問題。
換做 Java 12 中的 Switch 表達式,上述語句寫法如下:
清單 2. Switch 表達式示例
1
2
3
4
5
6
7
|
int dayNumber = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
default -> throw new IllegalStateException("Huh? " + day);
}
|
使用 Java 12 中 Switch 表達式的寫法,省去了 break 語句,避免了因少些 break 而出錯,同時將多個 case 合並到一行,顯得簡潔、清晰也更加優雅的表達邏輯分支,其具體寫法就是將之前的 case 語句表成了:case L ->
,即如果條件匹配 case L,則執行 標簽右側的代碼 ,同時標簽右側的代碼段只能是表達式、代碼塊或 throw 語句。為了保持兼容性,case 條件語句中依然可以使用字符 :
,這時 fall-through 規則依然有效的,即不能省略原有的 break 語句,但是同一個 Switch 結構里不能混用 ->
和 :
,否則會有編譯錯誤。並且簡化后的 Switch 代碼塊中定義的局部變量,其作用域就限制在代碼塊中,而不是蔓延到整個 Switch 結構,也不用根據不同的判斷條件來給變量賦值。
Java 11 以及之前版本中,Switch 表達式支持下面類型: byte、char、short、int、Byte、Character、Short、Integer、enum、tring,在未來的某個 Java 版本有可能會允許支持 float、double 和 long (以及上面類型的封裝類型)。
引入 JVM 常量 API
Java 12 中引入 JVM 常量 API,用來更容易地對關鍵類文件 (key class-file) 和運行時構件(artefact)的名義描述 (nominal description) 進行建模,特別是對那些從常量池加載的常量,這是一項非常技術性的變化,能夠以更簡單、標准的方式處理可加載常量。
此項改進主要在新的 java.lang.invoke.constant
包中定義了一系列基於值的符號引用類型,能夠描述每種可加載常量。符號引用以純粹 nominal 的形式描述可加載常量,與類加載或可訪問 性上下文分開。同時有些類可以作為自己的符號引用(例如 String),而對於可鏈接常量,另外定義了一系列符號引用類型,具體包括: ClassDesc (Class 的可加載常量標稱描述符) ,MethodTypeDesc(方法類型常量標稱描述符) ,MethodHandleDesc (方法句柄常量標稱描述符) 和 DynamicConstantDesc (動態常量標稱描述符) ,它們包含描述這些常量的 nominal 信息。
改進 AArch64 實現
Java 12 中將只保留一套 AArch64 實現,刪除所有與 arm64 實現相關的代碼,只保留 32 位 ARM 端口和 64 位 aarch64 的端口。刪除此套實現將允許所有開發人員將目標集中在剩下的這個 64 位 ARM 實現上,消除維護兩套端口所需的重復工作。
當前 Java 11 中存在兩套 64 位 AArch64 端口,它們主要存在於 src/hotspot/cpu/arm
和 open/src/hotspot/cpu/aarch64
目錄中。這兩套代碼中都實現了 AArch64,Java 12 中將刪除目錄 open/src/hotspot/cpu/arm
中關於 64-bit 的這套實現,只保留其中有關 32-bit 的實現,余下目錄的 open/src/hotspot/cpu/aarch64
代碼部分就成了 AArch64 的默認實現。
使用默認類數據共享(CDS)存檔
類數據共享機制 (Class Data Sharing ,簡稱 CDS) ,允許將一組類預處理為共享歸檔文件,以便在運行時能夠進行內存映射以減少 Java 程序的啟動時間,當多個 Java 虛擬機(JVM)共享相同的歸檔文件時,還可以減少動態內存的占用量,同時減少多個虛擬機在同一個物理或虛擬的機器上運行時的資源占用。
自 Java 8 以來,在基本 CDS 功能上進行了許多增強、改進,啟用 CDS 后應用的啟動時間和內存占用量顯着減少。使用 Java 11 早期版本在 64 位 Linux 平台上運行 HelloWorld 進行測試,測試結果顯示啟動時間縮短有 32 %,同時在其他 64 位平台上,也有類似或更高的啟動性能提升。
Java 12 針對 64 位平台下的 JDK 構建過程進行了增強改進,使其默認生成類數據共享(CDS)歸檔,以進一步達到改進應用程序的啟動時間的目的,同時也避免了需要手動運行:-Xshare:dump
的需要,修改后的 JDK 將在 lib/server
目錄中保留構建時生成的 CDS 存檔。
當然如果需要,也可以添加其他 GC 參數,來調整堆大小等,以獲得更優的內存分布情況,同時用戶也可以像之前一樣創建自定義的 CDS 存檔文件。
改善 G1 垃圾收集器,使其能夠中止混合集合
G1 是垃圾收集器,設計用於具有大量內存的多處理器機器,提高了垃圾回收效率。該垃圾收集器 設計的主要目標之一是滿足用戶設置的預期的 JVM 停頓時間,G1 采用一個高級分析引擎來選擇在收集期間要處理的工作量,此選擇過程的結果是一組稱為 GC 回收集的區域。一旦收集器確定了 GC 回收集 並且 GC 回收、整理工作已經開始,則 G1 收集器必須完成收集集合集的所有區域中的所有活動對象之后才能停止;但是如果收集器選擇過大的 GC 回收集,可能會導致 G1 回收器停頓時間超過預期時間。
Java 12 中將把 GC 回收集(混合收集集合)拆分為必需和可選兩部分,使 G1 垃圾回收器能中止垃圾回收過程。其中必需處理的部分包括 G1 垃圾收集器不能遞增處理的 GC 回收集的部分(如:年輕代),同時也可以包含老年代以提高處理效率。將 GC 回收集拆分為必需和可選部分時,需要為可選 GC 回收集部分維護一些其他數據,這會產生輕微的 CPU 開銷,但小於 1 %的變化,同時在 G1 回收器處理 GC 回收集期間,本機內存使用率也可能會增加,使用上述情況只適用於包含可選 GC 回收部分的 GC 混合回收集合。
在 G1 垃圾回收器完成收集需要必需回收的部分之后,便開始收集可選的部分,如果還有時間的話,但是粗粒度的處理,可選部分的處理粒度取決於剩余的時間,一次只能處理可選部分的一個子集區域。在完成可選收集部分的收集后,G1 垃圾回收器可以根據剩余時間決定是否停止收集。如果在處理完 必需處理的 部分后,屬於時間不足,總時間花銷接近預期時間,G1 垃圾回收器也可以中止可選部分的回收以達到滿足預期停頓時間的目標。
增強 G1 垃圾收集器,使其能自動返回未用堆內存給操作系統
上節中介紹了 Java 12 中增強了 G1 垃圾收集器關於混合收集集合的處理策略,這節主要介紹在 Java 12 中同時也對 G1 垃圾回收器進行了改進,使其能夠在空閑時自動將 Java 堆內存返還給操作系統,這也是 Java 12 中的另外一項重大改進。
目前 Java 11 版本中包含的 G1 垃圾收集器 暫時無法及時將已提交的 Java 堆內存返回給操作系統, G1 垃圾收集器僅在進行完整 GC (Full GC) 或並發處理周期時才能將 Java 堆返回內存。由於 G1 回收器盡可能避免完整 GC,並且只觸發基於 Java 堆占用和分配活動的並發周期,因此在許多情況下 G 1 垃圾回收器不能回收 Java 堆內存,除非有外部強制執行。
在使用雲平台的容器環境中,這種不利之處特別明顯。即使在虛擬機不活動,但如果仍然使用其分配的內存資源,哪怕是其中的一小部分,G1 回收器也仍將保留所有已分配的 Java 堆內存。而這將導致用戶需要始終為所有資源付費,哪怕是實際並未用到,而雲提供商也無法充分利用其硬件。如果在次期間虛擬機能夠檢測到 Java 堆內存的實際使用情況,並在利用空閑時間自動將 Java 堆內存返還,則兩者都將受益。
為了盡可能的向操作系統返回空閑內存,G1 垃圾收集器將在應用程序不活動期間定期生成或持續循環檢查整體 Java 堆使用情況,以便 G 1 垃圾收集器能夠更及時的將 Java 堆中不使用內存部分返還給操作系統。對於長時間處於空閑狀態的應用程序,此項改進將使 JVM 的內存利用率更加高效。
如果應用程序為非活動狀態,在下面兩種情況下,G1 回收器會觸發定期垃圾收集:
- 自上次垃圾回收完成 以來已超過
G1PeriodicGCInterva
l 毫秒, 並且此時沒有正在進行的垃圾回收任務。如果G1PeriodicGCInterval
值為零表示禁用快速回收內存的定期垃圾收集。 - 應用所在主機系統上執行方法
getloadavg()
,一分鍾內系統返回的平均負載值低於G1PeriodicGCSystemLoadThreshold
。如果G1PeriodicGCSystemLoadThreshold
值為零,則此條件不生效。
如果不滿足上述條件中的任何一個,則取消當期的定期垃圾回收。等一個 G1PeriodicGCInterval
時間周期后,將重新考慮是否執行定期垃圾回收。
G1 定期垃圾收集的類型根據 G1PeriodicGCInvokesConcurrent
參數的值確定:如果設置值了,G1 垃圾回收器將繼續上一個或者啟動一個新並發周期;如果沒有設置值,則 G1 回收器將執行一個完整的 GC。在每次一次 GC 回收末尾,G1 回收器將調整當前的 Java 堆大小,此時便有可能會將未使用內存返還給操作系統。新的 Java 堆內存大小根據現有配置確定,具體包括下列配置:- XX:MinHeapFreeRatio
、-XX:MaxHeapFreeRatio
、-Xms
、-Xmx
。
默認情況下,G1 回收器在定期垃圾回收期間新啟動或繼續上一輪並發周期,將最大限度地減少應用程序的中斷。如果定期垃圾收集嚴重影響程序執行,則需要考慮整個系統 CPU 負載,或讓用戶禁用定期垃圾收集。
結束語
自上次 Java 11 發布后,很快我們又迎來了 Java 12 版本的更新。Java 12 版本雖然是非 LTS 版本,但是這次更新也帶來了不少 JVM、GC 功能增強、改進,本文主要針對其中幾個影響重大變化以及主要的特性做了介紹。Java 12 已經來了,還跟得上更新的節奏嗎?
本文只是個人的一點思考,僅代表個人觀點,不代表作者所在單位的意見,如有不足之處,還望各位讀者能夠海涵,如可以,希望讀者們能夠反饋意見,交流心得,一同進步。
參考資源
- 參考 JDK 12,查看更多有關 Java 12 的最新信息。
- 參考 Shenandoah wiki page,查看更多有關 Shenandoah 的最新信息。
- 參考 Java 11 新特性介紹