JDK 14如期發佈,16個新特性快速預覽

JDK 14已經於2020年3月17日如期發佈。本文介紹JDK 14特性。

JEP 305: instanceof的模式匹配(預覽)

通過對instanceof運算符進行模式匹配來增強Java編程語言。 模式匹配允許程序中的通用邏輯,即從對象中有條件地提取組件,可以更簡潔,更安全地表示。 這是JDK 14中的預覽語言功能。

動機

幾乎每個程序都包含某種邏輯,這些邏輯結合了對表達式是否具有某種類型或結構的測試,然後有條件地提取其狀態的組件以進行進一步處理。例如,以下是在Java程序中常見的instanceof-and-cast用法:

if (obj instanceof String) {
    String s = (String) obj;
    // 使用s
}

上述示例中,爲了能夠安全地將obj轉爲我們期望的String類型,需要通過instanceof運算符對obj進行類型判斷。這裏發生了三件事:

  • 測試obj是否是一個String
  • 將obj轉換爲String
  • 聲明新的局部變量s,以便我們可以使用字符串值。

這種模式很簡單,並且所有Java程序員都可以理解,但是由於一些原因,它不是最優的。

  • 語法乏味
  • 同時執行類型檢測和類型轉換並不是必要的
  • String類型在程序中出現了3次,這混淆了後面更重要的邏輯
  • 重複的代碼容易滋生錯誤

在JDK 14中,上述代碼可以改爲下面的方式:

if (obj instanceof String s) {
    // 使用s
}

這樣整個代碼看上去更加簡潔。

描述

類型測試模式由指定類型的謂詞和單個綁定變量組成。在下面的代碼中,短語String是類型測試模式:

if (obj instanceof String s) {
    // 使用s
} else {
    // 不能使用s
}

如果obj是String的實例,則將其強制轉換爲String並分配給綁定變量s。綁定變量在if語句的true塊中,而不在if語句的false塊中。

與局部變量的範圍不同,綁定變量的範圍由包含的表達式和語句的語義確定。例如,在此代碼中:

if (!(obj instanceof String s)) {
    .. s.contains(..) ..
} else {
    .. s.contains(..) ..
}

true塊中的s表示封閉類中的字段,false塊中的s表示由instanceof運算符引入的綁定變量。

當if語句的條件變得比單個instanceof更復雜時,綁定變量的範圍也會相應地增長。 例如,在此代碼中:

if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..}

綁定變量s在&&運算符右側以及true塊中。僅當instanceof成功並分配給s時,才評估右側。

另一方面,在此代碼中:

if (obj instanceof String s || s.length() > 5) {.. s.contains(..) ..}

綁定變量s不在||右側的範圍內運算符,也不在true塊的範圍內。s指的是封閉類中的一個字段。

Joshua Bloch的經典著作Effective Java中有一段代碼示例:

@Override public boolean equals(Object o) { 
    return (o instanceof CaseInsensitiveString) && 
            ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); 
}

這段代碼可以使用新的語法寫成:

@Override public boolean equals(Object o) { 
    return (o instanceof CaseInsensitiveString cis) &&
            cis.s.equalsIgnoreCase(s); 
}

這個特性很有意思,因爲它爲更爲通用的模式匹配打開了大門。模式匹配通過更爲簡便的語法基於一定的條件來抽取對象的組件,而instanceof剛好是這種情況,它先檢查對象類型,然後再調用對象的方法或訪問對象的字段。

JEP 343: 打包工具(孵化)

該特性旨在創建一個用於打包獨立Java應用程序的工具。

動機

許多Java應用程序需要以一流的方式安裝在本機平臺上,而不是簡單地放置在類路徑或模塊路徑上。對於應用程序開發人員來說,交付簡單的JAR文件是不夠的。他們必須提供適合本機平臺的可安裝軟件包。這允許以用戶熟悉的方式分發,安裝和卸載Java應用程序。例如,在Windows上,用戶希望能夠雙擊一個軟件包來安裝他們的軟件,然後使用控制面板刪除該軟件。在macOS上,用戶希望能夠雙擊DMG文件並將其應用程序拖到Application文件夾中。

打包工具還可以幫助填補其他技術的空白,例如Java Web Start(已從JDK 11中刪除)和pack200(已在JDK 11中棄用,可能在以後的版本中刪除)。開發人員可以使用jlink將JDK分解爲所需的最小模塊集,然後使用打包工具生成一個壓縮的、可安裝的映像,該映像可以部署到目標計算機。

爲了以前滿足這些要求,JDK 8分發了一個名爲javapackager的打包工具。但是,作爲刪除JavaFX的一部分,該工具已從JDK 11中刪除。

描述

jpackage工具將Java應用程序打包到特定於平臺的程序包中,該程序包包含所有必需的依賴項。該應用程序可以作爲普通JAR文件的集合或作爲模塊的集合提供。受支持的特定於平臺的軟件包格式爲:

  • Linux:deb和rpm
  • macOS:pkg和dmg
  • Windows:MSI和EXE

默認情況下,jpackage會以最適合其運行系統的格式生成一個軟件包。

以下是基本用法:

$ jpackage --name myapp --input lib --main-jar main.jar

用法

1. 基本用法:非模塊化應用

假設你有一個由JAR文件組成的應用程序,所有應用程序都位於lib目錄下,並且主類在lib/main.jar中。下列命令

$ jpackage --name myapp --input lib --main-jar main.jar

將以本地系統的默認格式打包應用程序,並將生成的打包文件保留在當前目錄中。如果main.jar中的MANIFEST.MF文件沒有Main-Class屬性,我們必須顯式地指定主類:

$ jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main

打包的名稱是myapp。要啓動該應用程序,啓動器將從輸入目錄複製的每個JAR文件都放在JVM的類路徑上。

如果希望生成默認格式以外的軟件安裝包,可以使用--type選項。例如要在macOS上生成pkg文件(而不是dmg文件),我們可以使用下面的命令:

$ jpackage --name myapp --input lib --main-jar main.jar --type pkg

2. 基本用法:模塊化應用

如果你有一個模塊化應用程序,該應用程序由lib目錄中的模塊化JAR文件和/或JMOD文件組成,並且主類位於myapp模塊中,則下面的命令

$ jpackage --name myapp --module-path lib -m myapp

能夠將其打包。如果myapp模塊無法識別主類,則必須明確指定:

$ jpackage --name myapp --module-path lib -m myapp/myapp.Main

JEP 345: G1的NUMA內存分配優化

通過實現可識別NUMA的內存分配,提高大型計算機上的G1性能。

動機

現代的多插槽計算機越來越多地具有非統一的內存訪問(non-uniform memory access,NUMA),即內存與每個插槽或內核之間的距離並不相等。插槽之間的內存訪問具有不同的性能特徵,對更遠的插槽的訪問通常具有更大的延遲。

並行收集器中通過啓動-XX:+UseParallelGC能夠感知NUMA,這個功能已經實現了多年了,這有助於提高跨多插槽運行單個JVM的配置的性能。其他HotSpot收集器沒有此功能,這意味着他們無法利用這種垂直多路NUMA縮放功能。大型企業應用程序尤其傾向於在多個多插槽上以大堆配置運行,但是它們希望在單個JVM中運行具有可管理性優勢。 使用G1收集器的用戶越來越多地遇到這種擴展瓶頸。

描述

G1的堆組織爲固定大小區域的集合。一個區域通常是一組物理頁面,儘管使用大頁面(通過 -XX:+UseLargePages)時,多個區域可能組成一個物理頁面。

如果指定了+XX:+UseNUMA選項,則在初始化JVM時,區域將平均分佈在可用NUMA節點的總數上。

在開始時固定每個區域的NUMA節點有些不靈活,但是可以通過以下增強來緩解。爲了爲mutator線程分配新的對象,G1可能需要分配一個新的區域。它將通過從NUMA節點中優先選擇一個與當前線程綁定的空閒區域來執行此操作,以便將對象保留在新生代的同一NUMA節點上。如果在爲變量分配區域的過程中,同一NUMA節點上沒有空閒區域,則G1將觸發垃圾回收。要評估的另一種想法是,從距離最近的NUMA節點開始,按距離順序在其他NUMA節點中搜索自由區域。

該特性不會嘗試將對象保留在老年代的同一NUMA節點上。

此分配政策中不包括Humongous區。對於這些區,將不做任何特別的事情。

JEP 349: JFR事件流

公開JDK Flight Recorder數據以進行連續監視。

動機

HotSpot VM通過JFR產生的數據點超過500個,但是使用者只能通過解析日誌文件的方法使用它們。

用戶要想消費這些數據,必須開始一個記錄並停止,將內容轉儲到磁盤上,然後解析記錄文件。這對於應用程序分析非常有效,但是監控數據卻十分不方便(例如顯示動態更新數據的儀表盤)。

與創建記錄相關的開銷包括:

  • 發出在創建新記錄時必須發生的事件
  • 寫入事件元數據(例如字段佈局)
  • 寫入檢查點數據(例如堆棧跟蹤)
  • 將數據從磁盤存儲複製到單獨的記錄文件

如果有一種方法,可以在不創建新記錄文件的情況下,從磁盤存儲庫中讀取正在記錄的數據,就可以避免上述開銷。

描述

jdk.jfr模塊裏的jdk.jfr.consumer包,提供了異步訂閱事件的功能。用戶可以直接從磁盤存儲庫讀取記錄數據,也可以直接從磁盤存儲流中讀取數據,而無需轉儲記錄文件。可以通過註冊處理器(例如lambda函數)與流交互,從而對事件的到達進行響應。

下面的例子打印CPU的總體使用率,並持有鎖10毫秒。

try (var rs = new RecordingStream()) {
  rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
  rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
  rs.onEvent("jdk.CPULoad", event -> {
    System.out.println(event.getFloat("machineTotal"));
  });
  rs.onEvent("jdk.JavaMonitorEnter", event -> {
    System.out.println(event.getClass("monitorClass"));
  });
  rs.start();
}

RecordingStream類實現了接口jdk.jfr.consumer.EventStream,該接口提供了一種統一的方式來過濾和使用事件,無論源是實時流還是磁盤上的文件。

public interface EventStream extends AutoCloseable {
  public static EventStream openRepository();
  public static EventStream openRepository(Path directory);
  public static EventStream openFile(Path file);

  void setStartTime(Instant startTime);
  void setEndTime(Instant endTime);
  void setOrdered(boolean ordered);
  void setReuse(boolean reuse);

  void onEvent(Consumer<RecordedEvent> handler);
  void onEvent(String eventName, Consumer<RecordedEvent handler);
  void onFlush(Runnable handler);
  void onClose(Runnable handler);
  void onError(Runnable handler);
  void remove(Object handler);

  void start();
  void startAsync();

  void awaitTermination();
  void awaitTermination(Duration duration);
  void close();
}

創建流的方法有3種:

  • EventStream::openRepository(Path)從磁盤存儲庫中構造一個流。這是一種可以直接通過文件系統監視其他進程的方法。磁盤存儲庫的位置存儲在系統屬性jdk.jfr.repository中,可以使用API讀取到。
  • EventStream::openRepository()方法執行進程內監控。與RecordingStream不同,它不會開始錄製。相反,僅當通過外部方式(例如,使用JCMD或JMX)啓動記錄時,流才接收事件。
  • EventStream::openFile(Path)從記錄文件中創建流,擴充了已經存在的RecordingFile類。

該接口還可用於設置緩衝的數據量,以及是否應按時間順序對事件進行排序。爲了最大程度地降低分配壓力,還可以選擇控制是否應爲每個事件分配新的事件對象,或者是否可以重用以前的對象。我們可以在當前線程中啓動流,也可以異步啓動流。

JVM每秒一次將線程本地緩衝區中存儲的事件定期刷新到磁盤存儲庫。 一個單獨的線程解析最近的文件,直到寫入數據爲止,然後將事件推送給訂閱者。 爲了保持較低的開銷,僅從文件中讀取活動訂閱的事件。 要在刷新完成後收到通知,可以使用EventStream::onFlush(Runnable)方法註冊處理程序。 這是在JVM準備下一組事件時將數據聚合或推送到外部系統的機會。

JEP 352: 非易失性映射字節緩衝區

添加新的特定於JDK的文件映射模式,以便可以使用FileChannel API創建引用非易失性內存(non-volatile memory,NVM)的MappedByteBuffer實例。

動機

NVM爲應用程序程序員提供了在程序運行過程中創建和更新程序狀態的機會,而減少了輸出到持久性介質或從持久性介質輸入時的成本。這對於事務程序特別重要,在事務程序中,需要定期保持不確定狀態以啓用崩潰恢復。

現有的C庫(例如Intel的libpmem)爲C程序提供了對基層NVM的高效訪問。他們還以此爲基礎來支持對各種持久性數據類型的簡單管理。當前,由於頻繁需要進行系統調用或JNI調用來調用原始操作,從而確保內存更改是持久的,因此即使僅使用Java中的基礎庫也很昂貴。同樣的問題限制了高級庫的使用,並且由於C中提供的持久數據類型分配在無法從Java直接訪問的內存中這一事實而加劇了這一問題。與C或可以低成本鏈接到C庫的語言相比,這使Java應用程序和中間件(例如Java事務管理器)處於嚴重的劣勢。

該特性試圖通過允許映射到ByteBuffer的NVM的有效寫回來解決第一個問題。由於Java可以直接訪問ByteBuffer映射的內存,因此這可以通過實現與C語言中提供的客戶端庫等效的客戶端庫來解決第二個問題,以管理不同持久數據類型的存儲。

描述

1. 初步變更

該JEP使用了Java SE API的兩個增強功能:

  • 支持implementation-defined的映射模式
  • MppedByteBuffer::force方法以指定範圍

2. 特定於JDK的API更改

  • 通過新模塊中的公共API公開新的MapMode枚舉值

一個公共擴展枚舉ExtendedMapMode將添加到jdk.nio.mapmode程序包:

package jdk.nio.mapmode;
. . .
public class ExtendedMapMode {
    private ExtendedMapMode() { }

    public static final MapMode READ_ONLY_SYNC = . . .
    public static final MapMode READ_WRITE_SYNC = . . .
}

在調用FileChannel::map方法創建映射到NVM設備文件上的只讀或讀寫MappedByteBuffer時,可以使用上述的枚舉值。如果這些標誌在不支持NVM設備文件的平臺上傳遞,程序會拋出UnsupportedOperationException異常。在受支持的平臺上,僅當目標FileChannel實例是從通過NVM設備打開的派生文件時,才能傳遞這些參數。在任何其他情況下,都會拋出IOException異常。

  • 發佈BufferPoolMXBean,用於跟蹤MappedByteBuffer統計信息

JEP 358: 友好的空指針異常

精確描述哪個變量爲null,提高JVM生成的NullPointerException的可用性。

動機

每個Java開發人員都遇到過NullPointerException(NPE)問題。NPE幾乎可以出現在程序的任意位置,因此嘗試捕獲和修復它們是不可能的。下面的代碼:

a.i = 99;

JVM會打印出方法名、文件名和NPE異常的行數:

Exception in thread "main" java.lang.NullPointerException
    at Prog.main(Prog.java:5)

使用這個錯誤報告,開發人員可以定位到a.i = 99;並推斷對象a是null。但是對於更復雜的代碼,不使用調試器就無法確定哪個變量爲空。假設下面的代碼中出現了一個NPE:

a.b.c.i = 99;

僅僅使用文件名和行數,並不能精確定位到哪個變量爲null,是a、b還是c?

訪問數組也會發生類似的問題。假設此代碼中出現一個NPE:

a[i][j][k] = 99;

文件名和行號不能精確指出哪個數組組件爲空。是a還是a[i]a[i][j]

一行代碼可能包含多個訪問路徑,每個訪問路徑都可能是NPE的來源。假設此代碼中出現一個NPE:

a.i = b.j;

文件名和行號並不能確定哪個對象爲空,是a還是b?

NPE也可能在方法調用中傳遞,看下面的代碼:

x().y().i = 99;

文件名和行號不能指出哪個方法調用返回null。是x()還是y()?

描述

JVM在程序調用空引用的位置拋出NPE異常,通過分析程序的字節碼指令,JVM可以精確判斷哪個變量爲空,並在NPE中描述詳細信息(根據源代碼)。包含方法名、文件名和行號的null-detail消息將顯示在JVM的消息中。

例如a.i = 99;的NPE異常可能是如下格式:

Exception in thread "main" java.lang.NullPointerException: 
        Cannot assign field "i" because "a" is null
    at Prog.main(Prog.java:5)

在更復雜的a.b.c.i = 99;語句中,NPE消息會包含導致空值的完整訪問路徑:

Exception in thread "main" java.lang.NullPointerException: 
        Cannot read field "c" because "a.b" is null
    at Prog.main(Prog.java:5)

同樣,如果數組訪問和賦值語句a[i][j][k] = 99;引發NPE:

Exception in thread "main" java.lang.NullPointerException:
        Cannot load from object array because "a[i][j]" is null
    at Prog.main(Prog.java:5)

類似地,a.i = b.j;會引發NPE:

Exception in thread "main" java.lang.NullPointerException:
        Cannot read field "j" because "b" is null
    at Prog.main(Prog.java:5)

JEP 359: record(預覽)

通過record增強Java編程語言。record提供了一種緊湊的語法來聲明類,這些類是淺層不可變數據的透明持有者。

動機

我們經常聽到這樣的抱怨:“Java太冗長”、“Java規則過多”。首當其衝的就是充當簡單集合的“數據載體”的類。爲了寫一個數據類,開發人員必須編寫許多低價值、重複且容易出錯的代碼:構造函數、訪問器、equals()、hashCode()和toString()等等。

儘管IDE可以幫助開發人員編寫數據載體類的絕大多數編碼,但是這些代碼仍然冗長。

從表面上看,將Record是爲了簡化模板編碼而生的,但是它還有“遠大”的目標:modeling data as data(將數據建模爲數據)。record應該更簡單、簡潔、數據不可變。

描述

record是Java的一種新的類型。同枚舉一樣,record也是對類的一種限制。record放棄了類通常享有的特性:將API和表示解耦。但是作爲回報,record使數據類變得非常簡潔。

一個record具有名稱和狀態描述。狀態描述聲明瞭record的組成部分。例如:

record Point(int x, int y) { }

因爲record在語義上是數據的簡單透明持有者,所以記錄會自動獲取很多標準成員:

  • 狀態聲明中的每個成員,都有一個 private final的字段;
  • 狀態聲明中的每個組件的公共讀取訪問方法,該方法和組件具有相同的名字;
  • 一個公共的構造函數,其簽名與狀態聲明相同;
  • equals和hashCode的實現;
  • toString的實現。

限制

records不能擴展任何類,並且不能聲明私有字段以外的實例字段。聲明的任何其他字段都必須是靜態的。

records類都是隱含的final類,並且不能是抽象類。這些限制使得records的API僅由其狀態描述定義,並且以後不能被其他類實現或繼承。

在record中額外聲明變量

也可以顯式聲明從狀態描述自動派生的任何成員。可以在沒有正式參數列表的情況下聲明構造函數(這種情況下,假定與狀態描述相同),並且在正常構造函數主體正常完成時調用隱式初始化(this.x=x)。這樣就可以在顯式構造函數中僅執行其參數的驗證等邏輯,並省略字段的初始化,例如:

record Range(int lo, int hi) {
  public Range {
    if (lo > hi)  /* referring here to the implicit constructor parameters */
      throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
  }
}

JEP 361: Switch Expressions (標準)

擴展switch可以使其應用於語句或表達式。擴展switch使其可以用作語句或表達式,以便兩種形式都可以使用傳統的“case ... :”標籤(帶有貫穿)或“... ->”標籤(不帶有貫穿),還有另一個新語句,用於從switch表達式產生值。這些更改將簡化日常編碼,併爲在交換機中使用模式匹配提供了方法。這些功能在JDK 12和JDK 13中是屬於預覽語言功能,在JDK 14中正式稱爲標準。

動機

當我們準備增強Java編程語言以支持模式匹配(JEP 305)時,現有switch語句的一些不規則性(長期以來一直困擾着用戶)成爲了障礙。下面的代碼中,衆多的break語句使代碼變得冗長,這種“視覺噪聲”通常掩蓋了更多的錯誤。

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標籤“case L ->”,以表示如果匹配標籤,則只執行標籤右邊的代碼。switch標籤允許在每種情況下使用逗號分隔多個常量。現在可以這樣編寫以前的代碼:

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標籤“case L ->”右側的代碼被限制爲表達式、代碼塊或throw語句。這樣局部變量的範圍在本塊之內,而傳統的switch語句局部變量的作用域是整個模塊!

switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...     // The scope of 'temp' continues to the }
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...    // Can't call this variable 'temp'
        break;
    default:
        int temp3 = ...    // Can't call this variable 'temp'
}

許多現有的switch語句實質上是對switch表達式的模擬,其中每個分支要麼分配給一個公共目標變量,要麼返回一個值:

int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
}

上面的表述是複雜、重複且容易出錯的。代碼設計者的意圖是爲每天計算numLetters。這段代碼可以改寫成下面這段形式,更加清晰和安全:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

描述

1. “case L ->”標籤

除了傳統的“case L :”標籤外,還定義了一種更簡潔的形式:“case L ->”標籤。如果表達式匹配了某個標籤,則僅執行箭頭右側的表達式或語句;否則將不執行任何操作。

static void howMany(int k) {
    switch (k) {
        case 1  -> System.out.println("one");
        case 2  -> System.out.println("two");
        default -> System.out.println("many");
    }
}

下面的代碼:

howMany(1);
howMany(2);
howMany(3);

將會打印:

one
two
many

2. Switch表達式

JDK 14擴展了switch語句,使其可以應用於表達式中。例如上述的howMany方法可以重寫爲如下形式:

static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one";
            case  2 -> "two";
            default -> "many";
        }
    );
}

在通常情況下,switch表達式如下所示:

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};

3. 通過yield產生值

大多數switch表達式在“case L->”標籤的右側都有一個表達式。如果需要一個完整的塊,JDK 14引入了一個新的yield語句來產生一個值,該值成爲封閉的switch表達式的值。

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

JEP 362: 棄用Solaris和SPARC端口

不建議使用Solaris/SPARC,Solaris/x64和Linux/SPARC端口,以在將來的發行版中刪除它們。

動機

放棄對這些端口的支持將使OpenJDK社區中的貢獻者能夠加速新功能的開發,這些新功能將推動平臺向前發展。

JEP 363: 移除CMS垃圾收集器

移除CMS(Concurrent Mark Sweep)垃圾收集器。

動機

在兩年多以前的JEP 291中,就已經棄用了CMS收集器,並說明會在以後的發行版中刪除,以加快其他垃圾收集器的發展。在這段時間裏,我們看到了2個新的垃圾收集器ZGC和Shenandoah的誕生,同時對G1的進一步改進。G1自JDK 6開始便成爲CMS的繼任者。我們希望以後現有的收集器進一步減少對CMS的需求。

描述

此更改將禁用CMS的編譯,刪除源代碼中gc/cms目錄的內容,並刪除僅與CMS有關的選項。嘗試使用命令-XX:+UseConcMarkSweepGC開啓CMS會收到以下警告:

Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; \
support was removed in <version>

VM將使用默認收集器繼續執行。

JEP 364: macOS系統上的ZGC(實驗)

將ZGC垃圾收集器移植到macOS。

動機

儘管我們希望需要ZGC可伸縮性的用戶使用基於Linux的環境,但是在部署應用程序之前,開發人員通常會使用Mac進行本地開發和測試。 還有一些用戶希望運行桌面應用程序,例如帶有ZGC的IDE。

描述

ZGC的macOS實現由兩部分組成:

  • 支持macOS上的多映射內存。 ZGC設計大量使用彩色指針,因此在macOS上我們需要一種將多個虛擬地址(在算法中包含不同顏色)映射到同一物理內存的方法。我們將爲此使用mach microkernel mach_vm_remap API。堆的物理內存在單獨的地址視圖中維護,在概念上類似於文件描述符,但位於(主要是)連續的虛擬地址中。該內存被重新映射到內存的各種ZGC視圖中,代表了算法的不同指針顏色。
  • ZGC支持不連續的內存保留。在Linux上,我們在初始化期間保留16TB的虛擬地址空間。我們假設沒有共享庫將映射到所需的地址空間。在默認的Linux配置上,這是一個安全的假設。但是在macOS上,ASLR機制會侵入我們的地址空間,因此ZGC必須允許堆保留不連續。假設VM實現使用單個連續的內存預留,則共享的VM代碼也必須停止。如此一來,is_in_reserved(),reserved_region()和base()之類的GC API將從CollectedHeap中刪除。

JEP 365: Windows系統上的ZGC(實驗)

將ZGC垃圾收集器移植到Windows系統上。

描述

ZGC的大多數代碼庫都是平臺無關的,不需要Windows特定的更改。現有的x64負載屏障支持與操作系統無關,也可以在Windows上使用。需要移植的特定於平臺的代碼與如何保留地址空間以及如何將物理內存映射到保留的地址空間有關。用於內存管理的Windows API與POSIX API不同,並且在某些方面不太靈活。

Windows實現的ZGC需要進行以下工作:

  • 支持多映射內存。 ZGC使用彩色指針需要支持堆多重映射,以便可以從進程地址空間中的多個不同位置訪問同一物理內存。在Windows上,分頁文件支持的內存爲物理內存提供了一個標識(句柄),該標識與映射它的虛擬地址無關。使用此標識,ZGC可以將同一物理內存映射到多個位置。
  • 支持將分頁文件支持的內存映射到保留的地址空間。 Windows內存管理API不如POSIX的mmap/munmap靈活,尤其是在將文件支持的內存映射到以前保留的地址空間區域中時。爲此,ZGC將使用Windows概念的地址空間佔位符。 Windows 10和Windows Server版本1803中引入了佔位符概念。不會實現對Windows較早版本的ZGC支持。
  • 支持映射和取消映射堆的任意部分。 ZGC的堆佈局與其動態調整堆頁面大小(以及重新調整大小)相結合,需要支持映射和取消映射任意堆粒子。此要求與Windows地址空間佔位符結合使用時,需要特別注意,因爲佔位符必須由程序顯式拆分/合併,而不是由操作系統自動拆分/合併(如在Linux上)。
  • 支持提交和取消提交堆的任意部分。 ZGC可以在Java程序運行時動態地提交和取消提交物理內存。爲了支持這些操作,物理內存將被劃分爲多個分頁文件段並由其支持。每個分頁文件段都對應一個ZGC堆粒度,並且可以獨立於其他段進行提交和取消提交。

JEP 366: 棄用Parallel Scavenge和Serial Old垃圾收集算法的組合

棄用Parallel Scavenge和Serial Old垃圾收集算法的組合。

動機

有一組GC算法的組合很少使用,但是維護起來卻需要巨大的工作量:並行年輕代GC(ParallelScavenge)和串行老年代GC(SerialOld)的組合。用戶必須使用-XX:+UseParallelGC -XX:-UseParallelOldGC來啓用此組合。

這種組合是畸形的,因爲它將並行的年輕代GC算法和串行的老年代GC算法組合在一起使用。我們認爲這種組合僅在年輕代很多、老年代很少時纔有效果。在這種情況下,由於老年代的體積較小,因此完整的收集暫停時間是可以接受的。但是在生產環境中,這種方式是非常冒險的:年輕代的對象容易導致OutOfMemoryException。此組合的唯一優勢是總內存使用量略低。我們認爲,這種較小的內存佔用優勢(最多是Java堆大小的約3%)不足以超過維護此GC組合的成本。

描述

除了棄用選項組合-XX:+UseParallelGC -XX:-UseParallelOldGC外,我們還將棄用選項-XX:UseParallelOldGC,因爲它唯一的用途是取消選擇並行的舊版GC,從而啓用串行舊版GC。

因此,任何對UseParallelOldGC選項的明確使用都會顯示棄用警告。

JEP 367: 移除Pack200工具和API

刪除java.util.jar軟件包中的pack200和unpack200工具以及Pack200 API。這些工具和API在Java SE 11中已經被註明爲不推薦,並明確打算在將來的版本中刪除它們。

動機

Pack200是JSR 200在Java SE 5.0中引入的一種JAR文件壓縮方案。其目標是“減少Java應用程序打包,傳輸和交付的磁盤和帶寬需求”。開發人員使用一對工具pack200和unpack200壓縮和解壓縮其JAR文件。在java.util.jar包中提供了一個API。

刪除Pack200的三個原因:

  • 從歷史上看,通過56k調制解調器緩慢下載JDK阻礙了Java的採用。 JDK功能的不斷增長導致下載量膨脹,進一步阻礙了採用。使用Pack200壓縮JDK是緩解此問題的一種方法。但是,時間已經過去了:下載速度得到了提高,並且JDK 9爲Java運行時(JEP 220)和用於構建運行時的模塊(JMOD)引入了新的壓縮方案。因此,JDK 9和更高版本不依賴Pack200。 JDK 8是在構建時用pack200壓縮的最新版本,在安裝時用unpack200壓縮的最新版本。總之,Pack200的主要使用者(JDK本身)不再需要它。
  • 除了JDK,Pack200還可以壓縮客戶端應用程序,尤其是applet。某些部署技術(例如Oracle的瀏覽器插件)會自動解壓縮applet JAR。但是,客戶端應用程序的格局已經改變,並且大多數瀏覽器都放棄了對插件的支持。因此,Pack200的主要消費者類別(在瀏覽器中運行的小程序)不再是將Pack200包含在JDK中的驅動程序。
  • Pack200是一項複雜而精緻的技術。它的文件格式與類文件格式和JAR文件格式緊密相關,二者均以JSR 200所無法預料的方式發展。(例如,JEP 309向類文件格式添加了一種新的常量池條目,並且JEP 238在JAR文件格式中添加了版本控制元數據。)JDK中的實現是在Java和本機代碼之間劃分的,這使得維護變得很困難。 java.util.jar.Pack200中的API不利於Java SE平臺的模塊化,從而導致在Java SE 9中刪除了其四種方法。總的來說,維護Pack200的成本是巨大的,並且超過了其收益。包括在Java SE和JDK中。

描述

JDK最終針對的JDK功能發行版中將刪除以前用@Deprecated(forRemoval = true)註解的java.base模塊中的三種類型:

  • java.util.jar.Pack200
  • java.util.jar.Pack200.Packer
  • java.util.jar.Pack200.Unpacker

包含pack200和unpack200工具的jdk.pack模塊先前已使用@Deprecated(forRemoval = true)進行了註解,並且還將在此JEP最終針對的JDK功能版本中將其刪除。

JEP 368: Text Blocks(二次預覽)

Java語言增加文本塊功能。文本塊是多行字符串文字,能避免大多數轉義。

動機

在Java中,HTML, XML, SQL, JSON等字符串對象都很難閱讀和維護。

1. HTML

使用one-dimensional的字符串語法:

String html = "<html>\n" +
              "    <body>\n" +
              "        <p>Hello, world</p>\n" +
              "    </body>\n" +
              "</html>\n";

使用two-dimensional文本塊語法:

String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;

2. SQL

使用one-dimensional的字符串語法:

String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
               "WHERE `CITY` = 'INDIANAPOLIS'\n" +
               "ORDER BY `EMP_ID`, `LAST_NAME`;\n";

使用two-dimensional文本塊語法:

String query = """
               SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
               WHERE `CITY` = 'INDIANAPOLIS'
               ORDER BY `EMP_ID`, `LAST_NAME`;
               """;

3. 多語言示例

使用one-dimensional的字符串語法:

ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("function hello() {\n" +
                         "    print('\"Hello, world\"');\n" +
                         "}\n" +
                         "\n" +
                         "hello();\n");

使用two-dimensional文本塊語法:

ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("""
                         function hello() {
                             print('"Hello, world"');
                         }
                         
                         hello();
                         """);

描述

文本塊是Java語言的新語法,可以用來表示任何字符串,具有更高的表達能力和更少的複雜度。

文本塊的開頭定界符是由三個雙引號字符(""")組成的序列,後面跟0個或多個空格,最後跟一個行終止符。內容從開頭定界符的行終止符之後的第一個字符開始。

結束定界符是三個雙引號字符的序列。內容在結束定界符的第一個雙引號之前的最後一個字符處結束。

與字符串文字中的字符不同,文本塊的內容中可以直接包含雙引號字符。允許在文本塊中使用\",但不是必需的或不建議使用。

與字符串文字中的字符不同,內容可以直接包含行終止符。允許在文本塊中使用\n,但不是必需或不建議使用。例如,文本塊:

"""
line 1
line 2
line 3
"""

等效於字符串文字:

"line 1\nline 2\nline 3\n"

或字符串文字的串聯:

"line 1\n" +
"line 2\n" +
"line 3\n"

JEP 370: 外部存儲器API(孵化)

引入一個API,以允許Java程序安全有效地訪問Java堆之外的外部內存。

動機

許多Java的庫都能訪問外部存儲,例如Ignite、mapDB、memcached及Netty的ByteBuf API。這樣可以:

  • 避免垃圾回收相關成本和不可預測性
  • 跨多個進程共享內存
  • 通過將文件映射到內存中來序列化、反序列化內存內容。

但是Java API卻沒有提供一個令人滿意的訪問外部內存的解決方案。

Java 1.4中引入的ByteBuffer API允許創建直接字節緩衝區,這些緩衝區是按堆外分配的,並允許用戶直接從Java操作堆外內存。但是,直接緩衝區是有限的。

開發人員可以從Java代碼訪問外部內存的另一種常見途徑是使用sun.misc.Unsafe API。Unsafe有許多公開的內存訪問操作(例如Unsafe::getInt和putInt)。使用Unsafe訪問內存非常高效:所有內存訪問操作都定義爲JVM內在函數,因此JIT會定期優化內存訪問操作。然而根據定義,Unsafe API是不安全的——它允許訪問任何內存位置(例如,Unsafe::getInt需要很長的地址)。如果Java程序了訪問某些已釋放的內存位置,可能會使JVM崩潰。最重要的是,Unsafe API不是受支持的Java API,並且強烈建議不要使用它。

雖然也可以使用JNI訪問內存,但是與該解決方案相關的固有成本使其在實踐中很少適用。整個開發流程很複雜,因爲JNI要求開發人員編寫和維護C代碼段。 JNI本質上也很慢,因爲每次訪問都需要Java到native的轉換。

在訪問外部內存時,開發人員面臨一個難題:應該使用安全但受限(可能效率較低)的方法(例如ByteBuffer),還是應該放棄安全保證並接受不受支持和危險的Unsafe API?

該JEP引入了受支持的,安全且有效的外部內存訪問API。並且設計時就充分考慮了JIT優化。

描述

外部存儲器訪問API引入了三個主要的抽象:MemorySegment,MemoryAddress和MemoryLayout。

MemorySegment用於對具有給定空間和時間範圍的連續內存區域進行建模。可以將MemoryAddress視爲段內的偏移量,MemoryLayout是內存段內容的程序描述。

可以從多種來源創建內存段,例如本機內存緩衝區,Java數組和字節緩衝區(直接或基於堆)。例如,可以如下創建本機內存段:

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   ...
}

上述代碼將創建大小爲100字節的,與本機內存緩衝區關聯的內存段。

內存段在空間上受限制;任何試圖使用該段來訪問這些界限之外的內存的嘗試都會導致異常。正如使用try-with-resource構造所證明的那樣,片段在時間上也是有界的。也就是說,它們已創建,使用並在不再使用時關閉。關閉段始終是一個顯式操作,並且可能導致其他副作用,例如與該段關聯的內存的重新分配。任何訪問已關閉的內存段的嘗試都將導致異常。空間和時間安全性檢查對於確保內存訪問API的安全性至關重要。

通過獲取內存訪問var句柄可以取消引用與段關聯的內存。這些特殊的var句柄具有至少一個強制訪問座標,類型爲MemoryAddress,即發生取消引用的地址。它們是使用MemoryHandles類中的工廠方法獲得的。要設置本機段的元素,我們可以使用如下所示的內存訪問var句柄:

VarHandle intHandle = MemoryHandles.varHandle(int.class);

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   MemoryAddress base = segment.baseAddress();
   for (int i = 0 ; i < 25 ; i++) {
        intHandle.set(base.offset(i * 4), i);
   }
}

參考引用

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