生產升級JDK 17 必讀手冊

架構技術專欄發表於2023-12-29

原文點這裡,檢視更多優質文章

DK 17 在 2021 年 9 月 14 號正式釋出了!根據釋出的規劃,這次釋出的 JDK 17 是一個長期維護的版本(LTS)。

Java 17 提供了數千個效能穩定性安全性更新,以及 14 個 JEP(JDK 增強提案),進一步改進了 Java 語言和平臺,以幫助開發人員提高工作效率。

JDK 17 包括新的語言增強、庫更新、對新 Apple (Mx CPU)計算機的支援、舊功能的刪除和棄用,並努力確保今天編寫的 Java 程式碼在未來的 JDK 版本中繼續工作而不會發生變化。它還提供語言功能預覽和孵化 API,以收集 Java 社群的反饋

語言特性增強

密封的類和介面(正式版)

封閉類可以是封閉類和或者封閉介面,用來增強 Java 程式語言,防止其他類或介面擴充套件或實現它們。這個特性由Java 15的預覽版本晉升為正式版本。

  • 密封的類和介面解釋和應用

因為我們引入了sealed classinterfaces,這些class或者interfaces只允許被指定的類或者interface進行擴充套件和實現。

使用修飾符sealed,您可以將一個類宣告為密封類。密封的類使用reserved關鍵字permits列出可以直接擴充套件它的類。子類可以是最終的,非密封的或密封的。

之前我們的程式碼是這樣的。

public class Person { } //人
 
class Teacher extends Person { }//教師
 
class Worker extends Person { }  //工人
 
class Student extends Person{ } //學生

但是我們現在要限制 Person類 只能被這三個類繼承,不能被其他類繼承,需要這麼做。

// 新增sealed修飾符,permits後面跟上只能被繼承的子類名稱
public sealed class Person permits Teacher, Worker, Student{ } //人
 
// 子類可以被修飾為 final
final class Teacher extends Person { }//教師
 
// 子類可以被修飾為 non-sealed,此時 Worker類就成了普通類,誰都可以繼承它
non-sealed class Worker extends Person { }  //工人
// 任何類都可以繼承Worker
class AnyClass extends Worker{}
 
//子類可以被修飾為 sealed,同上
sealed class Student extends Person permits MiddleSchoolStudent,GraduateStudent{ } //學生
 
 
final class MiddleSchoolStudent extends Student { }  //中學生
 
final class GraduateStudent extends Student { }  //研究生

很強很實用的一個特性,可以限制類的層次結構。

  • 補充:它是由Amber專案孵化而來(會經歷兩輪以上預覽版本)

什麼是Amber專案?

Amber 專案的目標是探索和孵化更小的、以生產力為導向的 Java 語言功能,這些功能已被 OpenJDK JEP 流程接受為候選 JEP。本專案由 Compiler Group 贊助。 大多數 Amber 功能在成為 Java 平臺的正式部分之前至少要經過兩輪預覽。對於給定的功能,每輪預覽和最終標準化都有單獨的 JEP。此頁面僅連結到某個功能的最新 JEP。此類 JEP 可能會酌情連結到該功能的早期 JEP。

工具庫的更新

JEP 306:恢復始終嚴格的浮點語義

Java 程式語言和 Java 虛擬機器最初只有嚴格的浮點語義。從 Java 1.2 開始,預設情況下允許在這些嚴格語義中進行微小的變化,以適應當時硬體架構的限制。這些差異不再有幫助或必要,因此已被 JEP 306 刪除。

JEP 356:增強的偽隨機數生成器

為偽隨機數生成器 (PRNG) 提供新的介面型別和實現。這一變化提高了不同 PRNG 的互操作性,並使得根據需求請求演算法變得容易,而不是硬編碼特定的實現。簡單而言只需要理解如下三個問題: @pdai

JDK 17之前如何生成隨機數

  1. Random 類

典型的使用如下,隨機一個int值

// random int
new Random().nextInt();
​
/**
 * description 獲取指定位數的隨機數
 *
 * @param length 1
 * @return java.lang.String
 */
public static String getRandomString(int length) {
    String base = "abcdefghijklmnopqrstuvwxyz0123456789";
    Random random = new Random();
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < length; i++) {
        int number = random.nextInt(base.length());
        sb.append(base.charAt(number));
    }
    return sb.toString();
}
  1. ThreadLocalRandom 類

提供執行緒間獨立的隨機序列。它只有一個例項,多個執行緒用到這個例項,也會線上程內部各自更新狀態。它同時也是 Random 的子類,不過它幾乎把所有 Random 的方法又實現了一遍。

/**
 * nextInt(bound) returns 0 <= value < bound; repeated calls produce at
 * least two distinct results
 */
public void testNextIntBounded() {
    // sample bound space across prime number increments
    for (int bound = 2; bound < MAX_INT_BOUND; bound += 524959) {
        int f = ThreadLocalRandom.current().nextInt(bound);
        assertTrue(0 <= f && f < bound);
        int i = 0;
        int j;
        while (i < NCALLS &&
               (j = ThreadLocalRandom.current().nextInt(bound)) == f) {
            assertTrue(0 <= j && j < bound);
            ++i;
        }
        assertTrue(i < NCALLS);
    }
}
  1. SplittableRandom 類

非執行緒安全,但可以 fork 的隨機序列實現,適用於拆分子任務的場景。

/**
 * Repeated calls to nextLong produce at least two distinct results
 */
public void testNextLong() {
    SplittableRandom sr = new SplittableRandom();
    long f = sr.nextLong();
    int i = 0;
    while (i < NCALLS && sr.nextLong() == f)
        ++i;
    assertTrue(i < NCALLS);
}

為什麼需要增強

  1. 上述幾個類實現程式碼質量和介面抽象不佳
  2. 缺少常見的偽隨機演算法
  3. 自定義擴充套件隨機數的演算法只能自己去實現,缺少統一的介面

增強後是什麼樣的

程式碼的最佳化自不必說,我們就看下新增了哪些常見的偽隨機演算法

如何使用這個呢?可以使用RandomGenerator

RandomGenerator g = RandomGenerator.of("L64X128MixRandom");

JEP 382:新的macOS渲染管道

使用 Apple Metal API 為 macOS 實現 Java 2D 管道。新管道將減少 JDK 對已棄用的 Apple OpenGL API 的依賴。

目前預設情況下,這是禁用的,因此渲染仍然使用OpenGL API;要啟用metal,應用程式應透過設定系統屬性指定其使用:

-Dsun.java2d.metal=true

Metal或OpenGL的使用對應用程式是透明的,因為這是內部實現的區別,對Java API沒有影響。Metal管道需要macOS 10.14.x或更高版本。在早期版本上設定它的嘗試將被忽略。

新的平臺支援

JEP 391:支援macOS AArch64

將 JDK 移植到 macOS/AArch64 平臺。該埠將允許 Java 應用程式在新的基於 Arm 64 的 Apple Silicon 計算機上本地執行。

舊功能的刪除和棄用

JEP 398:棄用 Applet API

所有網路瀏覽器供應商要麼已取消對 Java 瀏覽器外掛的支援,要麼已宣佈計劃這樣做。 Applet API 已於 2017 年 9 月在 Java 9 中棄用,但並未移除。

JEP 407:刪除 RMI 啟用

刪除遠端方法呼叫 (RMI) 啟用機制,同時保留 RMI 的其餘部分。

JEP 410:刪除實驗性 AOT 和 JIT 編譯器

實驗性的基於 Java 的提前 (AOT) 和即時 (JIT) 編譯器是實驗性功能,並未得到廣泛採用。作為可選,它們已經從 JDK 16 中刪除。這個 JEP 從 JDK 原始碼中刪除了這些元件。

JEP 411:棄用安全管理器以進行刪除

安全管理器可以追溯到 Java 1.0。多年來,它一直不是保護客戶端 Java 程式碼的主要方法,也很少用於保護伺服器端程式碼。在未來的版本中將其刪除將消除重大的維護負擔,並使 Java 平臺能夠向前發展。

新功能的預覽和孵化API

JEP 406:新增switch模式匹配(預覽版)

允許針對多個模式測試表示式,每個模式都有特定的操作,以便可以簡潔安全地表達複雜的面向資料的查詢。

JEP 412:外部函式和記憶體api (第一輪孵化)

改進了 JDK 14 和 JDK 15 中引入的孵化 API,使 Java 程式能夠與 Java 執行時之外的程式碼和資料進行互操作。透過有效地呼叫外部函式(即 JVM 之外的程式碼)和安全地訪問外部記憶體,這些 API 使 Java 程式能夠呼叫本地庫和處理本地資料,而不會像 Java 本地介面 (JNI) 那樣脆弱和複雜。這些 API 正在巴拿馬專案中開發,旨在改善 Java 和非 Java 程式碼之間的互動。

JEP 414:Vector API(第二輪孵化)

如下內容來源於https://xie.infoq.cn/article/8304c894c4e38318d38ceb116,作者是九叔

AVX(Advanced Vector Extensions,高階向量擴充套件)實際上是 x86-64 處理器上的一套 SIMD(Single Instruction Multiple Data,單指令多資料流)指令集,相對於 SISD(Single instruction, Single dat,單指令流但資料流)而言,SIMD 非常適用於 CPU 密集型場景,因為向量計算允許在同一個 CPU 時鐘週期內對多組資料批次進行資料運算,執行效能非常高效,甚至從某種程度上來看,向量運算似乎更像是一種並行任務,而非像標量計算那樣,在同一個 CPU 時鐘週期內僅允許執行一組資料運算,存在嚴重的執行效率低下問題。

隨著 Java16 的正式來臨,開發人員可以在程式中使用 Vector API 來實現各種複雜的向量計算,由 JIT 編譯器 Server Compiler(C2)在執行期將其編譯為對應的底層 AVX 指令執行。當然,在講解如何使用 Vector API 之前,我們首先來看一個簡單的標量計算程式。示例:

void scalarComputation() {
    var a = new float[10000000];
    var b = new float[10000000];
    // 省略陣列a和b的賦值操作
    var c = new float[10000000];
    for (int i = 0; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}

在上述程式示例中,迴圈體內每次只能執行一組浮點運算,總共需要執行約 1000 萬次才能夠獲得最終的運算結果,可想而知,這樣的執行效率必然低效。值得慶幸的是,從 Java6 的時代開始,Java 的設計者們就在 HotSpot 虛擬機器中引入了一種被稱之為 SuperWord 的自動向量最佳化演算法,該演算法預設會將迴圈體內的標量計算自動最佳化為向量計算,以此來提升資料運算時的執行效率。當然,我們可以透過虛擬機器引數-XX:-UseSuperWord來顯式關閉這項最佳化(從實際測試結果來看,如果不開啟自動向量最佳化,存在約 20%~22%之間的效能下降)。

在此大家需要注意,儘管 HotSpot 預設支援自動向量最佳化,但侷限性仍然非常明顯,首先,JIT 編譯器 Server Compiler(C2)僅僅只會對迴圈體內的程式碼塊做向量最佳化,並且這樣的最佳化也是極不可靠的;其次,對於一些複雜的向量運算,SuperWord 則顯得無能為力。因此,在一些特定場景下(比如:機器學習,線性代數,密碼學等),建議大家還是儘可能使用 Java16 為大家提供的 Vector API 來實現複雜的向量計算。示例:

// 定義256bit的向量浮點運算
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;
void vectorComputation(float[] a, float[] b, float[] c) {
    var i = 0;
    var upperBound = SPECIES.loopBound(a.length);
    for (; i < upperBound; i += SPECIES.length()) {
        var va = FloatVector.fromArray(SPECIES, a, i);
        var vb = FloatVector.fromArray(SPECIES, b, i);
        var vc = va.mul(va).
                add(vb.mul(vb)).
                neg();
        vc.intoArray(c, i);
    }
    for (; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}

值得注意的是,Vector API 包含在 jdk.incubator.vector 模組中,程式中如果需要使用 Vector API 則需要在 module-info.java 檔案中引入該模組。:

module java16.test{
    requires jdk.incubator.vector;
}

JEP 389:外部連結器 API(孵化器)

該孵化器 API 提供了靜態型別、純 Java 訪問原生程式碼的特性,該 API 將大大簡化繫結原生庫的原本複雜且容易出錯的過程。Java 1.1 就已透過 Java 原生介面(JNI)支援了原生方法呼叫,但並不好用。Java 開發人員應該能夠為特定任務繫結特定的原生庫。它還提供了外來函式支援,而無需任何中間的 JNI 粘合程式碼。

JEP 393:外部儲存器訪問 API(第三次孵化)

在 Java 14 和 Java 15 中作為孵化器 API 引入的這個 API 使 Java 程式能夠安全有效地對各種外部儲存器(例如本機儲存器、永續性儲存器、託管堆儲存器等)進行操作。它提供了外部連結器 API 的基礎。

如下內容來源於https://xie.infoq.cn/article/8304c894c4e38318d38ceb116,作者是九叔

在實際的開發過程中,絕大多數的開發人員基本都不會直接與堆外記憶體打交道,但這並不代表你從未接觸過堆外記憶體,像大家經常使用的諸如:RocketMQ、MapDB 等中介軟體產品底層實現都是基於堆外儲存的,換句話說,我們幾乎每天都在間接與堆外記憶體打交道。那麼究竟為什麼需要使用到堆外記憶體呢?簡單來說,主要是出於以下 3 個方面的考慮:

  • 減少 GC 次數和降低 Stop-the-world 時間;
  • 可以擴充套件和使用更大的記憶體空間;
  • 可以省去實體記憶體和堆記憶體之間的資料複製步驟。

在 Java14 之前,如果開發人員想要操作堆外記憶體,通常的做法就是使用 ByteBuffer 或者 Unsafe,甚至是 JNI 等方式,但無論使用哪一種方式,均無法同時有效解決安全性和高效性等 2 個問題,並且,堆外記憶體的釋放也是一個令人頭痛的問題。以 DirectByteBuffer 為例,該物件僅僅只是一個引用,其背後還關聯著一大段堆外記憶體,由於 DirectByteBuffer 物件例項仍然是儲存在堆空間內,只有當 DirectByteBuffer 物件被 GC 回收時,其背後的堆外記憶體才會被進一步釋放。

在此大家需要注意,程式中透過 ByteBuffer.allocateDirect()方法來申請實體記憶體資源所耗費的成本遠遠高於直接在 on-heap 中的操作,而且實際開發過程中還需要考慮資料結構如何設計、序列化/反序列化如何支撐等諸多難題,所以與其使用語法層面的 API 倒不如直接使用 MapDB 等開源產品來得更實惠。

如今,在堆外記憶體領域,我們似乎又多了一個選擇,從 Java14 開始,Java 的設計者們在語法層面為大家帶來了嶄新的 Memory Access API,極大程度上簡化了開發難度,並得以有效的解決了安全性和高效性等 2 個核心問題。示例:

// 獲取記憶體訪問var控制程式碼
var handle = MemoryHandles.varHandle(char.class,
        ByteOrder.nativeOrder());
// 申請200位元組的堆外記憶體
try (MemorySegment segment = MemorySegment.allocateNative(200)) {
    for (int i = 0; i < 25; i++) {
        handle.set(segment, i << 2, (char) (i + 1 + 64));
        System.out.println(handle.get(segment, i << 2));
    }
}

關於堆外記憶體段的釋放,Memory Access API 提供有顯式和隱式 2 種方式,開發人員除了可以在程式中透過 MemorySegment 的 close()方法來顯式釋放所申請的記憶體資源外,還可以註冊 Cleaner 清理器來實現資源的隱式釋放,後者會在 GC 確定目標記憶體段不再可訪問時,釋放與之關聯的堆外記憶體資源。

參考文章


相關文章