關於 Java 18 你想知道的一切

個人創作公約:本人聲明創作的所有文章皆爲自己原創,如果有參考任何文章的地方,會標註出來,如果有疏漏,歡迎大家批判。如果大家發現網上有抄襲本文章的,歡迎舉報,並且積極向這個 github 倉庫 提交 issue,謝謝支持~

如果你不喜歡這個文字版的,可以參考官方做的這個 Java 內幕新聞第 20 期 - 關於 Java 18 你想知道的一切: -B站視頻地址 -知乎視頻地址

Java 18 於今天(2022-3-22)發佈 GA 版本了,今天也是我和我寶寶領證一週年的日子,爲了紀念今天,特此奉上 - 關於 Java 18 你想知道的一切

正式發佈的新特性

簡易 HTTP 服務器

相關 JEP:

爲了方便大家快速建立一個 HTTP 服務器來掛載一些靜態文件,實現快速簡易測試,演示某些功能,JDK 18 附帶了一個簡易的 HTTP 服務器 - 在 bin 目錄下多了一個工具 jwebserver

可以通過下面的命令行來啓動一個簡易的 HTTP 服務器:

image

可以指定的參數包括:

  • -b addr or --bind-address addr:指定綁定地址,默認 addr 是:127.0.0.1 or ::1 (loopback)
  • -d dir or --directory dir:指定掛載目錄,默認 dir 是當前目錄,掛載後可以獲取文件夾內的內容
  • -o level or --output level:指定日誌級別,默認 level 是 info(可以是:none | info | verbose)
  • -p port or --port port:指定端口,默認 port 是 8000

訪問可以看到這個相當於是掛載目錄的簡單文件服務器:

image

同時也能在啓動的 console 中看到請求的 accesslog:

127.0.0.1 - - [March 21st, 2022:14:25:48 +0800] "GET / HTTP/1.1" 200 -

它只服務於 HEAD 和 GET 請求,不支持身份驗證、訪問控制、加密等。

你可以通過使用 com.sun.net.httpserver 下的類,自定義這個 HTTP 服務器的配置,自定義 HttpHandler,Filter 這些,例如:

image

互聯網地址解析 SPI

相關 JEP:

原來 Java 中的互聯網地址解析是內置的解析器,即使用本地 'hosts' 文件和 DNS 的組合;Java 18 之後,爲互聯網地址解析定義了 SPI,這樣,'java.net.InetAddress' 可以使用除內置的解析器之外的解析器。

這個主要是爲了:

  • 爲 Project Loom 做準備:'java.net.InetAddress' 的解析操作目前在操作系統調用中阻塞。這對於 Loom 的虛擬線程來說是個問題,因爲這也會阻塞虛擬線程使得調度器無法切換到另一個虛擬線程。通過抽象這個爲 SPI 來提供另一個解析器實現非阻塞的 DNS 解析。
  • 兼容新的網絡協議:可以實現新的解析協議的無縫集成,比如 DNS over QUIC/TLS/HTTPS。
  • 定製改造解析結果:使框架和應用程序能夠更好地控制解析結果,並允許現有的庫使用自定義解析器進行改造。
  • 更好的測試:比如你可以實現自己的 SPI 模擬遠程請求實際解析到本地的某些地址等等。

這個 SPI 究竟是哪個類,可以參考 java.util.ServiceLoader 的使用,通過裏面的 api 指定如下 SPI 接口的實現:java.net.spi.InetAddressResolverProvider

Finalization 的 Deprecate For Removal

相關 JEP:

Java finalization 是 Java 一開始就有的特性,當初設計出來的時候是爲了讓我們避免資源泄漏:當沒有人引用保存資源的實例時然後執行一段代碼來回收資源。本着這個思路,就會聯想到垃圾回收器知道什麼時候是要回收一個對象,所以就利用垃圾回收的機制來執行這段代碼就好了。所以,設計出 Object 的 finalize() 方法,Java 類可以覆蓋這個方法,在裏面填寫關閉資源的代碼。這段代碼會在對象被回收的某個時候被調用。但是這種機制帶來了如下幾個問題:

  • 假設你的 JVM 老年代增長的很慢,如果你的需要 finalize 的對象進入了老年代,那麼可能很久對象都不會被回收。
  • 假設你的需要 finalize 的對象突然增多,創建這種對象的速度要快於 GC 進行收集以及執行 finalize() 方法的速度,這樣會造成雪崩
  • 由於無法確定哪個線程執行 finalize() 方法,按照什麼順序執行這些 finalize() 方法,因此在這個方法中不能有影響線程安全的代碼,以及亂引用外部對象導致對象又“復活”了

並且,這種 Finalization 還是一個歷史包袱,所有的垃圾回收器代碼都要不斷維護這個執行這些 finalize() 方法的機制,影響了這些垃圾回收器的迭代,並且由於 Finalization 的存在導致 GC 要佔用的內存頁增加了,ZGC 估計 1.5% 的內存佔用只是爲了 Finalization 用的。

所以,其實從 Java 9 開始就標記 Object 的 finalize() 方法爲 Deprecated 了,現在從 Java 18 開始,正式標記爲 Deprecated for removal,也就是不久的將來,這個方法會被完全去掉。

如何驗證移除 Finalization 對你的項目是否有影響?

如果你使用了 JFR,可以通過 Java 18 後加入的 JFR 事件 jdk.FinalizerStatistics,來看出你的 JVM 中是否有 Finalization

如果你沒有開啓 JFR,那麼我推薦你使用下 JFR,很好用,參考:JFR 全解

如果你不想通過 JFR,那麼你可以先在你的程序運行的時候,記錄下:

  • JVM 內存使用情況,建議開啓 Native Memory Tracking,參考:JVM相關 - 深入理解 System.gc()
  • 進程相關的文件描述符數量
  • Direct Buffer 以及 MMAP Buffer 使用量:可以通過 JMX 的 MBean 查看,例如: image

記錄好之後,啓動參數加上 --finalization=disabled,這個參數讓所有的 Finalization 機制失效,對比下內存用量,判斷是否依賴了 Finalization。

默認編碼爲 UTF-8

相關 JEP:

Java 中很多方法都帶有字符編碼集的參數,例如:

new String(new byte[10]);
new String(new byte[10], Charset.defaultCharset());

如果不傳的話,就是使用系統的默認字符集,例如 Linux, MacOS 上面一般是 UTF-8,Windows 上面就不是 UTF-8 了。從 Java 18 開始,默認字符集不再和操作系統有關,就是 UTF-8

如果你的運行操作系統就是 Linux, MacOS,或者你的啓動參數本身有 -Dfile.encoding=COMPAT 那麼基本對你沒有任何影響。

如果你想改回原來那種根據操作系統環境指定默認字符集的方式,可以使用這個啓動參數:-Dfile.encoding=COMPAT

通過方法句柄(MethodHandle)重新實現 Java 反射接口

相關 JEP:

在 JDK 18 之前,有三種用於反射操作的 JDK 內部機制:

  • 虛擬機本地方法
  • 動態生成的字節碼 stub (Method::invoke, Constructor::newInstance) 和依賴 Unsafe 類的字段訪問(Field::get and set):主要在 java.lang.reflect
  • 方法句柄(MethodHandle 類):主要在 java.lang.invoke

每次給 Java 添加一些新的結構特性,例如 Record 這些,都需要同時修改這三個的代碼,太費勁了。所以 Java 18 中通過使用 java.lang.invoke 下的類實現了第二種的這些 API,來減少未來添加新的語言特性所需要的工作量。這也是爲了 Project Valhalla 的原生值類型(可以棧上分配,類似於 c 語言的 struct,還有其他語言的 inline class)做準備。

可編譯的 Javadoc 代碼段

相關 JEP:

乾淨整潔更新及時並且有規範的示例的 API 文檔會讓你獲益良多,並且如果 API 文檔的代碼如果能編譯,能隨着你的源碼變化而變化,就更完美了,Java 18 就給了 Javadoc 這些特性。

我們編寫一個 Maven 項目試一下(代碼庫地址:https://github.com/HashZhang/code-snippet-test )

首先,我們想在普通 maven 項目的 src/main/javasrc/test/java 以外新添加一個目錄 src/demo/java 用於存放示例代碼。因爲示例代碼我們並不想打包到最後發佈的 jar 包,示例代碼也需要編譯,所以我們把這個示例代碼目錄標記爲測試代碼目錄(爲啥不放入 src/test/java,因爲我們還是想區分開示例代碼與單元測試代碼的): image 我們需要 maven 插件來執行生成 javadoc,同時我們要指定代碼段掃描的目錄(即你的源碼中,執行代碼段文件所處於的目錄,這個目錄我們這裏和源碼目錄 src/main/java 隔離開了,是 src/demo/java): image

首先,我們創建我們的 API 類,即:

image 可以看到,我們在註釋中指定了代碼段讀取的文件以及讀取的區域,我們現在來編寫示例代碼:

image

從示例代碼中,我們可以看到對於引用區域的指定(位於 @start@end 之間).

目前項目結構是: image

執行 mvn javadoc:javadoc,在 target/site 目錄下就能看到生成的 Javadoc,Javadoc 中可以包含你項目中的代碼段: image

你還可以高亮你的一些註釋,或者使用 CSS 編輯樣式,這裏就不再贅述了

預覽的新特性

Switch 模式匹配(第二次預覽)

Java 17 中的第一次預覽 Java 18 中的第二次預覽

Java 17 中正式發佈了 Sealed Class(封閉類),在這特性的基礎上,我們可以在 Switch 中進行模式匹配了,舉一個簡單的例子:

在某些情況下,我們可能想枚舉一個接口的所有實現類,例如:

image

我們如何能確定我們枚舉完了所有的 Shape 呢? Sealed Class 這個特性爲我們解決這個問題,Sealed Class 可以在聲明的時候就決定這個類可以被哪些類繼承:

image

Sealed Class (可以是 abstract class 或者 interface )在聲明時需要指定所有的實現類的名稱。針對繼承類,有如下限制:

  • Sealed Class 的繼承類必須和 Sealed Class 在同一個模塊下,如果沒有指定模塊,就必須在同一個包下
  • 每個繼承類必須直接繼承 Sealed Class,不能間接繼承
  • 每個繼承類必須是下面三種之一:
    • final 的 class,Java Record 本身就是 final 的
    • sealed 的 class,可以進一步指定會被哪些子類實現
    • non-sealed 的 class,也是一種擴展,但是打破 Sealed Class 的限制,Sealed Class 不知道也不關心這種的繼承類還會有哪些子類。

舉個例子即:

image

加入了 Switch 模式匹配之後,上面的 area 方法就可以改寫成(我們需要在編譯參數和啓動參數中加上 --enable-preview 啓用預覽): image

如果你這裏不寫 default,並且,少了一種類型的話,例如: image

那麼就會報編譯錯誤,這就是 switch 模式匹配的窮舉性檢查

在第二次預覽中,主要修復了針對包含參數泛型的封閉類的窮舉性檢查,即有如下封閉類:

image

對於下面的代碼,窮舉性檢查就不會誤報編譯錯誤了: image

這個特性還在不斷改善,大家可以試一下,並可以向這裏提意見交流:https://mail.openjdk.java.net/pipermail/amber-spec-experts/2022-February/003240.html

孵化中的新特性

外部函數與內存 API(第二次孵化)

相關 JEP:

這個是 Project Panama(取名自巴拿馬運河)帶來的一個很重要的孵化中的特性,就像連接太平洋和大西洋的巴拿馬運河一樣,Project Panama 希望將 Java 虛擬機與外部的非 Java 庫連接起來。這個特性就是其中最重要的一部分

這個特性主要目的是:

  • 首先,提供具有類似性能和安全特性 ByteBuffer API 的替代方案,修補了原有 API 的一些缺點(很多需要訪問堆外內存的庫,例如 Netty 操作直接內存作爲緩存池這種,都會從中受益)
  • 其次,通過用更多面向 Java 的 API 來代替 JNI,使本機庫更易訪問,也就是你可以通過 Java 代碼直接調用系統庫
  • 最後,統一替換掉 sun.misc.Unsafe 裏面關於內存訪問的 API,換成了更易於使用的封裝。

這裏有一個例子,是關於使用 Foreign Linker API 實現使用 Java 直接調用 Windows 上面的 user32 庫裏面的 MessageBoxW 函數:

image

感興趣可以看一下這個視頻:Foreign Linker API: Java native access without C | Modern Java | JDK16 |Head Crashing Informatics 27

目前,一些有意思的正在使用外部函數與內存 API 實驗的項目:

Vector API(第三次孵化)

相關 JEP:

這也是 Project Panama 中的一個重要組成部分。其中最主要的應用就是使用了 CPU 的 SIMD(單指令多數據)處理,它提供了通過程序的多通道數據流,可能有 4 條通道或 8 條通道或任意數量的單個數據元素流經的通道。並且 CPU 一次在所有通道上並行組織操作,這可以極大增加 CPU 吞吐量。通過 Vector API,Java 團隊正在努力讓 Java 程序員使用 Java 代碼直接訪問它;過去,他們必須在彙編代碼級別對向量數學進行編程,或者使用 C/C++ 與 Intrinsic 一起使用,然後通過 JNI 提供給 Java。

一個主要的優化點就是循環,過去的循環(標量循環),一次在一個元素上執行,那很慢。現在,您可以使用 Vector API 將標量算法轉換爲速度更快的數據並行算法。一個使用 Vector 的例子:

image

注意使用處於孵化的 Java 特性需要加上額外的啓動參數將模塊暴露,這裏是--add-modules jdk.incubator.vector,需要在 javac 編譯和 java 運行都加上這些參數,使用 IDEA 即:

image

image

測試結果:

image

其他使用,請參考:fizzbuzz-simd-style,這是一篇比較有意思的文章(雖然這個性能優化感覺不只由於 SIMD,還有算法優化的功勞,哈哈)

關於一些更加詳細的使用,以及設計思路,可以參考這個音頻:Vector API: SIMD Programming in Java

微信搜索“我的編程喵”關注公衆號,每日一刷,輕鬆提升技術,斬獲各種offer

我會經常發一些很好的各種框架的官方社區的新聞視頻資料並加上個人翻譯字幕到如下地址(也包括上面的公衆號),歡迎關注:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章