Java 13新特性概述

Java 13 已如期於 2019 年 9 月 17 日正式發佈,此次更新是繼半年前 Java 12 這大版本發佈之後的一次常規版本更新,在這一版中,主要帶來了 ZGC 增強、更新 Socket 實現、Switch 表達式更新等方面的改動、增強。

本文主要針對 Java 13 中主要的新特性展開介紹,帶你快速瞭解 Java 13 帶來的不同體驗。

動態應用程序類-數據共享

在 Java 10 中,爲了改善應用啓動時間和內存空間佔用,通過使用 APP CDS,加大了 CDS 的使用範圍,允許自定義的類加載器也可以加載自定義類給多個 JVM 共享使用,具體介紹可以參考 Java 10 新特性介紹 一文詳細介紹,在此就不再繼續展開 。

Java 13 中對 Java 10 中引入的 應用程序類數據共享進行了進一步的簡化、改進和擴展,即:允許在 Java 應用程序執行結束時動態進行類歸檔,具體能夠被歸檔的類包括:所有已被加載,但不屬於默認基層 CDS 的應用程序類和引用類庫中的類。通過這種改進,可以提高應用程序類-數據使用上的簡易性,減少在使用類-數據存檔中需要爲應用程序創建類加載列表的必要,簡化使用類-數據共享的步驟,以便更簡單、便捷地使用 CDS 存檔。

在 Java 中,如果要執行一個類,首先需要將類編譯成對應的字節碼文件,以下是 JVM 裝載、執行等需要的一系列準備步驟:假設給定一個類名,JVM 將在磁盤上查找到該類對應的字節碼文件,並將其進行加載,驗證字節碼文件,準備,解析,初始化,根據其內部數據結構加載到內存中。當然,這一連串的操作都需要一些時間,這在 JVM 啓動並且需要加載至少幾百個甚至是數千個類時,加載時間就尤其明顯。

Java 10 中的 App CDS 主要是爲了將不變的類數據,進行一次創建,然後存儲到歸檔中,以便在應用重啓之後可以對其進行內存映射而直接使用,同時也可以在運行的 JVM 實例之間共享使用。但是在 Java 10 中使用 App CDS 需要進行如下操作:

  1. 創建需要進行類歸檔的類列表
  2. 創建歸檔
  3. 使用歸檔方式啓動
    在使用歸檔文件啓動時,JVM 將歸檔文件映射到其對應的內存中,其中包含所需的大多數類,而

需要使用多麼複雜的類加載機制。甚至可以在併發運行的 JVM 實例之間共享內存區域,通過這種方式可以釋放需要在每個 JVM 實例中創建相同信息時浪費的內存,從而節省了內存空間。

在 Java 12 中,默認開啓了對 JDK 自帶 JAR 包類的存檔,如果想關閉對自帶類庫的存檔,可以在啓動參數中加上:

-Xshare:off

而在 Java 13 中,可以不用提供歸檔類列表,而是通過更簡潔的方式來創建包含應用程序類的歸檔。具體可以使用參數 -XX:ArchiveClassesAtExit 來控制應用程序在退出時生成存檔,也可以使用 -XX:SharedArchiveFile 來使用動態存檔功能,詳細使用見如下示例。

清單 1. 創建存檔文件示例

$ java -XX:ArchiveClassesAtExit=helloworld.jsa -cp helloworld.jar Hello

清單 2. 使用存檔文件示例

$ java -XX:SharedArchiveFile=hello.jsa -cp helloworld.jar Hello

上述就是在 Java 應用程序執行結束時動態進行類歸檔,並且在 Java 10 的基礎上,將多條命令進行了簡化,可以更加方便地使用類歸檔功能。

增強 ZGC 釋放未使用內存

ZGC 是 Java 11 中引入的最爲矚目的垃圾回收特性,是一種可伸縮、低延遲的垃圾收集器,不過在 Java 11 中是實驗性的引入,主要用來改善 GC 停頓時間,並支持幾百 MB 至幾個 TB 級別大小的堆,並且應用吞吐能力下降不會超過 15%,目前只支持 Linux/x64 位平臺的這樣一種新型垃圾收集器。

通過在實際中的使用,發現 ZGC 收集器中並沒有像 Hotspot 中的 G1 和 Shenandoah 垃圾收集器一樣,能夠主動將未使用的內存釋放給操作系統的功能。對於大多數應用程序來說,CPU 和內存都屬於有限的緊缺資源,特別是現在使用的雲上或者虛擬化環境中。如果應用程序中的內存長期處於空閒狀態,並且還不能釋放給操作系統,這樣會導致其他需要內存的應用無法分配到需要的內存,而這邊應用分配的內存還處於空閒狀態,處於"忙的太忙,閒的太閒"的非公平狀態,並且也容易導致基於虛擬化的環境中,因爲這些實際並未使用的資源而多付費的情況。由此可見,將未使用內存釋放給系統主內存是一項非常有用且亟需的功能。

ZGC 堆由一組稱爲 ZPages 的堆區域組成。在 GC 週期中清空 ZPages 區域時,它們將被釋放並返回到頁面緩存 ZPageCache 中,此緩存中的 ZPages 按最近最少使用(LRU)的順序,並按照大小進行組織。在 Java 13 中,ZGC 將向操作系統返回被標識爲長時間未使用的頁面,這樣它們將可以被其他進程重用。同時釋放這些未使用的內存給操作系統不會導致堆大小縮小到參數設置的最小大小以下,如果將最小和最大堆大小設置爲相同的值,則不會釋放任何內存給操作系統。

Java 13 中對 ZGC 的改進,主要體現在下面幾點:

  1. 釋放未使用內存給操作系統
  2. 支持最大堆大小爲 16TB
  3. 添加參數:-XX:SoftMaxHeapSize 來軟限制堆大小
    這裏提到的是軟限制堆大小,是指 GC 應努力是堆大小不要超過指定大小,但是如果實際需要,也還是允許 GC 將堆大小增加到超過 SoftMaxHeapSize 指定值。主要用在下面幾種情況:當希望降低堆佔用,同時保持應對堆空間臨時增加的能力,亦或想保留充足內存空間,以能夠應對內存分配,而不會因爲內存分配意外增加而陷入分配停滯狀態。不應將 SoftMaxHeapSize 設置爲大於最大堆大小(-Xmx 的值,如果未在命令行上設置,則此標誌應默認爲最大堆大小。

Java 13 中,ZGC 內存釋放功能,默認情況下是開啓的,不過可以使用參數:-XX:-ZUncommit 顯式關閉,同時如果將最小堆大小 (-Xms) 配置爲等於最大堆大小 (-Xmx),則將隱式禁用此功能。

還可以使用參數:-XX:ZUncommitDelay = (默認值爲 300 秒)來配置延遲釋放,此延遲時間可以指定釋放多長時間之前未使用的內存。

Socket API 重構

Java 中的 Socket API 已經存在了二十多年了,儘管這麼多年來,一直在維護和更新中,但是在實際使用中遇到一些侷限性,並且不容易維護和調試,所以要對其進行大修大改,才能跟得上現代技術的發展,畢竟二十多年來,技術都發生了深刻的變化。Java 13 爲 Socket API 帶來了新的底層實現方法,並且在 Java 13 中是默認使用新的 Socket 實現,使其易於發現並在排除問題同時增加可維護性。

Java Socket API(java.net.ServerSocket 和 java.net.Socket)包含允許監聽控制服務器和發送數據的套接字對象。可以使用 ServerSocket 來監聽連接請求的端口,一旦連接成功就返回一個 Socket 對象,可以使用該對象讀取發送的數據和進行數據寫回操作,而這些類的繁重工作都是依賴於 SocketImpl 的內部實現,服務器的發送和接收兩端都基於 SOCKS 進行實現的。

在 Java 13 之前,通過使用 PlainSocketImpl 作爲 SocketImpl 的具體實現。

Java 13 中的新底層實現,引入 NioSocketImpl 的實現用以替換 SocketImpl 的 PlainSocketImpl 實現,此實現與 NIO(新 I/O)實現共享相同的內部基礎結構,並且與現有的緩衝區高速緩存機制集成在一起,因此不需要使用線程堆棧。除了這些更改之外,還有其他一些更便利的更改,如使用 java.lang.ref.Cleaner 機制來關閉套接字(如果 SocketImpl 實現在尚未關閉的套接字上被進行了垃圾收集),以及在輪詢時套接字處於非阻塞模式時處理超時操作等方面。

爲了最小化在重新實現已使用二十多年的方法時出現問題的風險,在引入新實現方法的同時,之前版本的實現還未被移除,可以通過使用下列系統屬性以重新使用原實現方法:

-Djdk.net.usePlainSocketImpl = true

另外需要注意的是,SocketImpl 是一種傳統的 SPI 機制,同時也是一個抽象類,並未指定具體的實現,所以,新的實現方式嘗試模擬未指定的行爲,以達到與原有實現兼容的目的。但是,在使用新實現時,有些基本情況可能會失敗,使用上述系統屬性可以糾正遇到的問題,下面兩個除外。

  • 老版本中,PlainSocketImpl 中的 getInputStream()和 getOutputStream()方法返回的 InputStream 和 OutputStream 分別來自於其對應的擴展類型 FileInputStream 和 FileOutputStream,而這個在新版實現中則沒有。
  • 使用自定義或其它平臺的 SocketImpl 的服務器套接字無法接受使用其他(自定義或其它平臺)類型 SocketImpl 返回 Sockets 的連接。

通過這些更改,Java Socket API 將更易於維護,更好地維護將使套接字代碼的可靠性得到改善。同時 NIO 實現也可以在基礎層面完成,從而保持 Socket 和 ServerSocket 類層面上的不變。

Switch 表達式擴展(預覽功能)

在 Java 12 中引入了 Switch 表達式作爲預覽特性,而在 Java 13 中對 Switch 表達式做了增強改進,在塊中引入了 yield 語句來返回值,而不是使用 break。這意味着,Switch 表達式(返回值)應該使用 yield,而 Switch 語句(不返回值)應該使用 break,而在此之前,想要在 Switch 中返回內容,還是比較麻煩的,只不過目前還處於預覽狀態。

在 Java 13 之後,Switch 表達式中就多了一個關鍵字用於跳出 Switch 塊的關鍵字 yield,主要用於返回一個值,它和 return 的區別在於:return 會直接跳出當前循環或者方法,而 yield 只會跳出當前 Switch 塊,同時在使用 yield 時,需要有 default 條件。

在 Java 12 之前,傳統 Switch 語句寫法爲:

清單 3. 傳統形式

private static String getText(int number) {
    String result = "";
    switch (number) {
        case 1, 2:
        result = "one or two";
        break;
        case 3:
        result = "three";
        break;
        case 4, 5, 6:
        result = "four or five or six";
        break;
        default:
        result = "unknown";
        break;
    };
    return result;
}

在 Java 12 之後,關於 Switch 表達式的寫法改進爲如下:

清單 4. 標籤簡化形式

private static String getText(int number) {
    String result = switch (number) {
        case 1, 2 -> "one or two";
        case 3 -> "three";
        case 4, 5, 6 -> "four or five or six";
        default -> "unknown";
    };
    return result;
}

而在 Java 13 中,,value break 語句不再被編譯,而是用 yield 來進行值返回,上述寫法被改爲如下寫法:

清單 5. yield 返回值形式

private static String getText(int number) {
    return switch (number) {
        case 1, 2:
            yield "one or two";
        case 3:
            yield "three";
        case 4, 5, 6:
            yield "four or five or six";
        default:
            yield "unknown";
    };
}

文本塊(預覽功能)

一直以來,Java 語言在定義字符串的方式是有限的,字符串需要以雙引號開頭,以雙引號結尾,這導致字符串不能夠多行使用,而是需要通過換行轉義或者換行連接符等方式來變通支持多行,但這樣會增加編輯工作量,同時也會導致所在代碼段難以閱讀、難以維護。

Java 13 引入了文本塊來解決多行文本的問題,文本塊以三重雙引號開頭,並以同樣的以三重雙引號結尾終止,它們之間的任何內容都被解釋爲字符串的一部分,包括換行符,避免了對大多數轉義序列的需要,並且它仍然是普通的 java.lang.String 對象,文本塊可以在 Java 中可以使用字符串文字的任何地方使用,而與編譯後的代碼沒有區別,還增強了 Java 程序中的字符串可讀性。並且通過這種方式,可以更直觀地表示字符串,可以支持跨越多行,而且不會出現轉義的視覺混亂,將可以廣泛提高 Java 類程序的可讀性和可寫性。

在 Java 13 之前,多行字符串寫法爲:

清單 6. 多行字符串寫法

String html ="<html>\n" +
              "   <body>\n" +
              "      <p>Hello, World</p>\n" +
              "   </body>\n" +
              "</html>\n";
 
               
 String json ="{\n" +
              "   \"name\":\"mkyong\",\n" +
              "   \"age\":38\n" +
              "}\n";

在 Java 13 引入文本塊之後,寫法爲:

清單 7. 多行文本塊寫法

String html = """
                <html>
                    <body>
                        <p>Hello, World</p>
                    </body>
                </html>
                """;
 
 String json = """
                {
                    "name":"mkyong",
                    "age":38
                }
                """;

文本塊是作爲預覽功能引入到 Java 13 中的,這意味着它們不包含在相關的 Java 語言規範中,這樣做的好處是方便用戶測試功能並提供反饋,後續更新可以根據反饋來改進功能,或者必要時甚至刪除該功能,如果該功能立即成爲 Java SE

準的一部分,則進行更改將變得更加困難。重要的是要意識到預覽功能不是 beta 形式。

由於預覽功能不是規範的一部分,因此有必要爲編譯和運行時明確啓用它們。需要使用下面兩個命令行參數來啓用預覽功能:

清單 8. 啓用預覽功能

$ javac --enable-preview --release 13 Example.java
$ java --enable-preview Example

結束語

Java 在更新發布週期爲每半年發佈一次之後,在合併關鍵特性、快速得到開發者反饋等方面,做得越來越好。從 Java 11 到 Java 13,目前確實是嚴格保持半年更新的節奏。Java 13 版本的發佈帶來了些新特性和功能增強、性能提升和改進嘗試,不過 Java 13 不是 LTS 版本,本文針對其中對使用人員影響重大的以及主要的特性做了介紹,如有興趣,您可以自行下載相關代碼,繼續深入研究。

參考資源

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