Java 11 新特性介紹
Java 11 已於 2018 年 9 月 25 日正式發布,之前在 Java 10 新特性介紹中介紹過,為了加快的版本迭代、跟進社區反饋,Java 的版本發布周期調整為每六個月一次——即每半年發布一個大版本,每個季度發布一個中間特性版本,並且做出不會跳票的承諾。通過這樣的方式,Java 開發團隊能夠將一些重要特性盡早的合並到 Java Release 版本中,以便快速得到開發者的反饋,避免出現類似 Java 9 發布時的兩次延期的情況。
按照官方介紹,新的版本發布周期將會嚴格按照時間節點,於每年的 3 月和 9 月發布,Java 11 發布的時間節點也正好處於 Java 8 免費更新到期的前夕。與 Java 9 和 Java 10 這兩個被稱為"功能性的版本"不同,Java 11 僅將提供長期支持服務(LTS, Long-Term-Support),還將作為 Java 平台的默認支持版本,並且會提供技術支持直至 2023 年 9 月,對應的補丁和安全警告等支持將持續至 2026 年。
本文主要針對 Java 11 中的新特性展開介紹,讓您快速了解 Java 11 帶來的變化。
基於嵌套的訪問控制
與 Java 語言中現有的嵌套類型概念一致, 嵌套訪問控制是一種控制上下文訪問的策略,允許邏輯上屬於同一代碼實體,但被編譯之后分為多個分散的 class 文件的類,無需編譯器額外的創建可擴展的橋接訪問方法,即可訪問彼此的私有成員,並且這種改進是在 Java 字節碼級別的。
在 Java 11 之前的版本中,編譯之后的 class 文件中通過 InnerClasses 和 Enclosing Method 兩種屬性來幫助編譯器確認源碼的嵌套關系,每一個嵌套的類會編譯到自己所在的 class 文件中,不同類的文件通過上面介紹的兩種屬性的來相互連接。這兩種屬性對於編譯器確定相互之間的嵌套關系已經足夠了,但是並不適用於訪問控制。這里大家可以寫一段包含內部類的代碼,並將其編譯成 class 文件,然后通過 javap 命令行來分析,礙於篇幅,這里就不展開討論了。
Java 11 中引入了兩個新的屬性:一個叫做 NestMembers 的屬性,用於標識其它已知的靜態 nest 成員;另外一個是每個 nest 成員都包含的 NestHost 屬性,用於標識出它的 nest 宿主類。
標准 HTTP Client 升級
Java 11 對 Java 9 中引入並在 Java 10 中進行了更新的 Http Client API 進行了標准化,在前兩個版本中進行孵化的同時,Http Client 幾乎被完全重寫,並且現在完全支持異步非阻塞。
新版 Java 中,Http Client 的包名由 jdk.incubator.http 改為 java.net.http,該 API 通過 CompleteableFutures 提供非阻塞請求和響應語義,可以聯合使用以觸發相應的動作,並且 RX Flow 的概念也在 Java 11 中得到了實現。現在,在用戶層請求發布者和響應發布者與底層套接字之間追蹤數據流更容易了。這降低了復雜性,並最大程度上提高了 HTTP / 1 和 HTTP / 2 之間的重用的可能性。
Java 11 中的新 Http Client API,提供了對 HTTP/2 等業界前沿標准的支持,同時也向下兼容 HTTP/1.1,精簡而又友好的 API 接口,與主流開源 API(如:Apache HttpClient、Jetty、OkHttp 等)類似甚至擁有更高的性能。與此同時它是 Java 在 Reactive-Stream 方面的第一個生產實踐,其中廣泛使用了 Java Flow API,終於讓 Java 標准 HTTP 類庫在擴展能力等方面,滿足了現代互聯網的需求,是一個難得的現代 Http/2 Client API 標准的實現,Java 工程師終於可以擺脫老舊的 HttpURLConnection 了。下面模擬 Http GET 請求並打印返回內容:
清單 1. GET 請求示例
|
1
2
3
4
5
6
7
8
|
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();
|
Epsilon:低開銷垃圾回收器
Epsilon 垃圾回收器的目標是開發一個控制內存分配,但是不執行任何實際的垃圾回收工作。它提供一個完全消極的 GC 實現,分配有限的內存資源,最大限度的降低內存占用和內存吞吐延遲時間。
Java 版本中已經包含了一系列的高度可配置化的 GC 實現。各種不同的垃圾回收器可以面對各種情況。但是有些時候使用一種獨特的實現,而不是將其堆積在其他 GC 實現上將會是事情變得更加簡單。
下面是 no-op GC 的幾個使用場景:
- 性能測試:什么都不執行的 GC 非常適合用於 GC 的差異性分析。no-op (無操作)GC 可以用於過濾掉 GC 誘發的性能損耗,比如 GC 線程的調度,GC 屏障的消耗,GC 周期的不合適觸發,內存位置變化等。此外有些延遲者不是由於 GC 引起的,比如 scheduling hiccups, compiler transition hiccups,所以去除 GC 引發的延遲有助於統計這些延遲。
- 內存壓力測試:在測試 Java 代碼時,確定分配內存的閾值有助於設置內存壓力常量值。這時 no-op 就很有用,它可以簡單地接受一個分配的內存分配上限,當內存超限時就失敗。例如:測試需要分配小於 1G 的內存,就使用-Xmx1g 參數來配置 no-op GC,然后當內存耗盡的時候就直接 crash。
- VM 接口測試:以 VM 開發視角,有一個簡單的 GC 實現,有助於理解 VM-GC 的最小接口實現。它也用於證明 VM-GC 接口的健全性。
- 極度短暫 job 任務:一個短聲明周期的 job 任務可能會依賴快速退出來釋放資源,這個時候接收 GC 周期來清理 heap 其實是在浪費時間,因為 heap 會在退出時清理。並且 GC 周期可能會占用一會時間,因為它依賴 heap 上的數據量。
- 延遲改進:對那些極端延遲敏感的應用,開發者十分清楚內存占用,或者是幾乎沒有垃圾回收的應用,此時耗時較長的 GC 周期將會是一件壞事。
- 吞吐改進:即便對那些無需內存分配的工作,選擇一個 GC 意味着選擇了一系列的 GC 屏障,所有的 OpenJDK GC 都是分代的,所以他們至少會有一個寫屏障。避免這些屏障可以帶來一點點的吞吐量提升。
Epsilon 垃圾回收器和其他 OpenJDK 的垃圾回收器一樣,可以通過參數 -XX:+UseEpsilonGC 開啟。
Epsilon 線性分配單個連續內存塊。可復用現存 VM 代碼中的 TLAB 部分的分配功能。非 TLAB 分配也是同一段代碼,因為在此方案中,分配 TLAB 和分配大對象只有一點點的不同。Epsilon 用到的 barrier 是空的(或者說是無操作的)。因為該 GC
執行任何的 GC 周期,不用關系對象圖,對象標記,對象復制等。引進一種新的 barrier-set 實現可能是該 GC 對 JVM 最大的變化。
簡化啟動單個源代碼文件的方法
Java 11 版本中最令人興奮的功能之一是增強 Java 啟動器,使之能夠運行單一文件的 Java 源代碼。此功能允許使用 Java 解釋器直接執行 Java 源代碼。源代碼在內存中編譯,然后由解釋器執行。唯一的約束在於所有相關的類必須定義在同一個 Java 文件中。
此功能對於開始學習 Java 並希望嘗試簡單程序的人特別有用,並且能與 jshell 一起使用,將成為任何初學者學習語言的一個很好的工具集。不僅初學者會受益,專業人員還可以利用這些工具來探索新的語言更改或嘗試未知的 API。
如今單文件程序在編寫小實用程序時很常見,特別是腳本語言領域。從中開發者可以省去用 Java 編譯程序等不必要工作,以及減少新手的入門障礙。在基於 Java 10 的程序實現中可以通過三種方式啟動:
- 作為 * .class 文件
- 作為 * .jar 文件中的主類
- 作為模塊中的主類
而在最新的 Java 11 中新增了一個啟動方式,即可以在源代碼中聲明類,例如:如果名為 HelloWorld.java 的文件包含一個名為 hello.World 的類,那么該命令:
$ java HelloWorld.java
也等同於:
$ javac HelloWorld.java
$ java -cp . hello.World
|
用於 Lambda 參數的局部變量語法
在 Lambda 表達式中使用局部變量類型推斷是 Java 11 引入的唯一與語言相關的特性,這一節,我們將探索這一新特性。
從 Java 10 開始,便引入了局部變量類型推斷這一關鍵特性。類型推斷允許使用關鍵字 var 作為局部變量的類型而不是實際類型,編譯器根據分配給變量的值推斷出類型。這一改進簡化了代碼編寫、節省了開發者的工作時間,因為不再需要顯式聲明局部變量的類型,而是可以使用關鍵字 var,且不會使源代碼過於復雜。
可以使用關鍵字 var 聲明局部變量,如下所示:
var s = "Hello Java 11";
System.out.println(s);
|
但是在 Java 10 中,還有下面幾個限制:
- 只能用於局部變量上
- 聲明時必須初始化
- 不能用作方法參數
- 不能在 Lambda 表達式中使用
Java 11 與 Java 10 的不同之處在於允許開發者在 Lambda 表達式中使用 var 進行參數聲明。乍一看,這一舉措似乎有點多余,因為在寫代碼過程中可以省略 Lambda 參數的類型,並通過類型推斷確定它們。但是,添加上類型定義同時使用 @Nonnull 和 @Nullable 等類型注釋還是很有用的,既能保持與局部變量的一致寫法,也不丟失代碼簡潔。
Lambda 表達式使用隱式類型定義,它形參的所有類型全部靠推斷出來的。隱式類型 Lambda 表達式如下:
(x, y) -> x.process(y)
Java 10 為局部變量提供隱式定義寫法如下:
var x = new Foo();
for (var x : xs) { ... }
try (var x = ...) { ... } catch ...
|
為了 Lambda 類型表達式中正式參數定義的語法與局部變量定義語法的不一致,且為了保持與其他局部變量用法上的一致性,希望能夠使用關鍵字 var 隱式定義 Lambda 表達式的形參:
(var x, var y) -> x.process(y)
於是在 Java 11 中將局部變量和 Lambda 表達式的用法進行了統一,並且可以將注釋應用於局部變量和 Lambda 表達式:
@Nonnull var x = new Foo();
(@Nonnull var x, @Nullable var y) -> x.process(y)
|
低開銷的 Heap Profiling
Java 11 中提供一種低開銷的 Java 堆分配采樣方法,能夠得到堆分配的 Java 對象信息,並且能夠通過 JVMTI 訪問堆信息。
引入這個低開銷內存分析工具是為了達到如下目的:
- 足夠低的開銷,可以默認且一直開啟
- 能通過定義好的程序接口訪問
- 能夠對所有堆分配區域進行采樣
- 能給出正在和未被使用的 Java 對象信息
對用戶來說,了解它們堆里的內存分布是非常重要的,特別是遇到生產環境中出現的高 CPU、高內存占用率的情況。目前有一些已經開源的工具,允許用戶分析應用程序中的堆使用情況,比如:Java Flight Recorder、jmap、YourKit 以及 VisualVM tools.。但是這些工具都有一個明顯的不足之處:無法得到對象的分配位置,headp dump 以及 heap histogram 中都沒有包含對象分配的具體信息,但是這些信息對於調試內存問題至關重要,因為它能夠告訴開發人員他們的代碼中發生的高內存分配的確切位置,並根據實際源碼來分析具體問題,這也是 Java 11 中引入這種低開銷堆分配采樣方法的原因。
支持 TLS 1.3 協議
Java 11 中包含了傳輸層安全性(TLS)1.3 規范(RFC 8446)的實現,替換了之前版本中包含的 TLS,包括 TLS 1.2,同時還改進了其他 TLS 功能,例如 OCSP 裝訂擴展(RFC 6066,RFC 6961),以及會話散列和擴展主密鑰擴展(RFC 7627),在安全性和性能方面也做了很多提升。
新版本中包含了 Java 安全套接字擴展(JSSE)提供 SSL,TLS 和 DTLS 協議的框架和 Java 實現。目前,JSSE API 和 JDK 實現支持 SSL 3.0,TLS 1.0,TLS 1.1,TLS 1.2,DTLS 1.0 和 DTLS 1.2。
同時 Java 11 版本中實現的 TLS 1.3,重新定義了以下新標准算法名稱:
- TLS 協議版本名稱:TLSv1.3
- SSLContext 算法名稱:TLSv1.3
- TLS 1.3 的 TLS 密碼套件名稱:TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384
- 用於 X509KeyManager 的 keyType:RSASSA-PSS
- 用於 X509TrustManager 的 authType:RSASSA-PSS
還為 TLS 1.3 添加了一個新的安全屬性 jdk.tls.keyLimits。當處理了特定算法的指定數據量時,觸發握手后,密鑰和 IV 更新以導出新密鑰。還添加了一個新的系統屬性 jdk.tls.server.protocols,用於在 SunJSSE 提供程序的服務器端配置默認啟用的協議套件。
之前版本中使用的 KRB5密碼套件實現已從 Java 11 中刪除,因為該算法已不再安全。同時注意,TLS 1.3 與以前的版本不直接兼容。
升級到 TLS 1.3 之前,需要考慮如下幾個兼容性問題:
- TLS 1.3 使用半關閉策略,而 TLS 1.2 以及之前版本使用雙工關閉策略,對於依賴於雙工關閉策略的應用程序,升級到 TLS 1.3 時可能存在兼容性問題。
- TLS 1.3 使用預定義的簽名算法進行證書身份驗證,但實際場景中應用程序可能會使用不被支持的簽名算法。
- TLS 1.3 再支持 DSA 簽名算法,如果在服務器端配置為僅使用 DSA 證書,則無法升級到 TLS 1.3。
- TLS 1.3 支持的加密套件與 TLS 1.2 和早期版本不同,若應用程序硬編碼了加密算法單元,則在升級的過程中需要修改相應代碼才能升級使用 TLS 1.3。
- TLS 1.3 版本的 session 用行為及秘鑰更新行為與 1.2 及之前的版本不同,若應用依賴於 TLS 協議的握手過程細節,則需要注意。
ZGC:可伸縮低延遲垃圾收集器
ZGC 即 Z Garbage Collector(垃圾收集器或垃圾回收器),這應該是 Java 11 中最為矚目的特性,沒有之一。ZGC 是一個可伸縮的、低延遲的垃圾收集器,主要為了滿足如下目標進行設計:
- GC 停頓時間不超過 10ms
- 即能處理幾百 MB 的小堆,也能處理幾個 TB 的大堆
- 應用吞吐能力不會下降超過 15%(與 G1 回收算法相比)
- 方便在此基礎上引入新的 GC 特性和利用 colord
- 針以及 Load barriers 優化奠定基礎
- 當前只支持 Linux/x64 位平台
停頓時間在 10ms 以下,10ms 其實是一個很保守的數據,即便是 10ms 這個數據,也是 GC 調優幾乎達不到的極值。根據 SPECjbb 2015 的基准測試,128G 的大堆下最大停頓時間才 1.68ms,遠低於 10ms,和 G1 算法相比,改進非常明顯。
圖 1. 回收算法停頓時間對比

本圖片引用自:The Z Garbage Collector - An Introduction
不過目前 ZGC 還處於實驗階段,目前只在 Linux/x64 上可用,如果有足夠的需求,將來可能會增加對其他平台的支持。同時作為實驗性功能的 ZGC 將不會出現在 JDK 構建中,除非在編譯時使用 configure 參數:--with-jvm-features=zgc 顯式啟用。
在實驗階段,編譯完成之后,已經迫不及待的想試試 ZGC,需要配置以下 JVM 參數,才能使用 ZGC,具體啟動 ZGC 參數如下:
-XX:+ UnlockExperimentalVMOptions -XX:+ UseZGC -Xmx10g
其中參數:-Xmx 是 ZGC 收集器中最重要的調優選項,大大解決了程序員在 JVM 參數調優上的困擾。ZGC 是一個並發收集器,必須要設置一個最大堆的大小,應用需要多大的堆,主要有下面幾個考量:
- 對象的分配速率,要保證在 GC 的時候,堆中有足夠的內存分配新對象。
- 一般來說,給 ZGC 的內存越多越好,但是也不能浪費內存,所以要找到一個平衡。
飛行記錄器
飛行記錄器之前是商業版 JDK 的一項分析工具,但在 Java 11 中,其代碼被包含到公開代碼庫中,這樣所有人都能使用該功能了。
Java 語言中的飛行記錄器類似飛機上的黑盒子,是一種低開銷的事件信息收集框架,主要用於對應用程序和 JVM 進行故障檢查、分析。飛行記錄器記錄的主要數據源於應用程序、JVM 和 OS,這些事件信息保存在單獨的事件記錄文件中,故障發生后,能夠從事件記錄文件中提取出有用信息對故障進行分析。
啟用飛行記錄器參數如下:
-XX:StartFlightRecording
也可以使用 bin/jcmd 工具啟動和配置飛行記錄器:
清單 2. 飛行記錄器啟動、配置參數示例
|
1
2
3
|
$ jcmd <
pid
> JFR.start
$ jcmd <
pid
> JFR.dump filename=recording.jfr
$ jcmd <
pid
> JFR.stop
|
JFR 使用測試:
清單 3. JFR 使用示例
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class FlightRecorderTest extends Event {
@Label("Hello World")
@Description("Helps the programmer getting started")
static class HelloWorld extends Event {
@Label("Message")
String message;
}
public static void main(String[] args) {
HelloWorld event = new HelloWorld();
event.message = "hello, world!";
event.commit();
}
}
|
在運行時加上如下參數:
java -XX:StartFlightRecording=duration=1s, filename=recording.jfr
下面讀取上一步中生成的 JFR 文件:recording.jfr
清單 4. 飛行記錄器分析示例
|
1
2
3
4
5
6
7
|
public void readRecordFile() throws IOException {
final Path path = Paths.get("D:\\ java \\recording.jfr");
final List<
RecordedEvent
> recordedEvents = RecordingFile.readAllEvents(path);
for (RecordedEvent event : recordedEvents) {
System.out.println(event.getStartTime() + "," + event.getValue("message"));
}
}
|
動態類文件常量
為了使 JVM 對動態語言更具吸引力,Java 的第七個版本已將 invokedynamic 引入其指令集。
過 Java 開發人員通常不會注意到此功能,因為它隱藏在 Java 字節代碼中。通過使用 invokedynamic,可以延遲方法調用的綁定,直到第一次調用。例如,Java 語言使用該技術來實現 Lambda 表達式,這些表達式僅在首次使用時才顯示出來。這樣做,invokedynamic 已經演變成一種必不可少的語言功能。
Java 11 引入了類似的機制,擴展了 Java 文件格式,以支持新的常量池:CONSTANT_Dynamic,它在初始化的時候,像 invokedynamic
令生成代理方法一樣,委托給 bootstrap 方法進行初始化創建,對上層軟件沒有很大的影響,降低開發新形式的可實現類文件約束帶來的成本和干擾。
結束語
Java 在更新發布周期為每半年發布一次之后,在合並關鍵特性、快速得到開發者反饋等方面,做得越來越好。Java 11 版本的發布也帶來了不少新特性和功能增強、性能提升、基礎能力的全面進步和突破,本文針對其中對使用人員影響重大的以及主要的特性做了介紹。Java 12 即將到來,您准備好了嗎?
本文僅代表作者個人觀點,不代表其所在單位的意見,如有不足之處,還望您能夠海涵。希望您能夠反饋意見,交流心得,一同進步。
參考資源
- 參考 JDK 11,查看更多有關 Java 10 的最新信息。
- 參考 The Z Garbage Collector - An Introduction,查看更多有關 ZGC 的最新信息。
- Java 10 新特性介紹
- 輕松遷移至 Java 11
- Java 12 新特性概述
