2021年畢昇 JDK 的第一個重要更新來了

語言: CN / TW / HK

2021 年 3 月 31 日,畢昇 JDK update 版本正式釋出,下載方式見文末參考文件[1][2],該版本在同步 OpenJDK 社群 8u282/11.0.10 的基礎上,還包含如下更新,為使用者提供高效能、可用於生產環境的 OpenJDK 發行版。

  1. G1 Full GC 優化(畢昇 JDK 11)

  2. LazyBox 特性(畢昇 JDK 11)

  3. 提供鯤鵬硬體加速的 KAEProvider(畢昇 JDK 8)

  4. Jmap 併發掃描優化(畢昇 JDK8, 畢昇 JDK11)

  5. Bug fixes

G1 Full GC 優化原理及使用

G1 GC(Garbage First Garbage Collector)是一款面向服務端、低延遲的垃圾收集器.當 G1 進行 Full GC 時,會對整個堆進行清理,耗時較長,所以如何調優 Full gc 一直是開發人員奮鬥的目標之一。G1 Full GC 在當前的實現中,主要包括如下四個階段:標記存活物件、計算目標物件的位置、更新引用的位置、移動物件完成壓縮,由於 G1 對存活物件較多的 region 進行回收時(G1 是以 region 為單位來管理 java 堆的),需要移動較多的物件,卻只能回收較少的記憶體,效率低下,因此可以在 G1 的第四階段不對此 region 進行壓縮,減少處理的時間。

優化原理

針對上面這種情況,可以通過如下步驟,對 Full GC 過程進行優化:

  • 在標記階段,對每個 region 存活的位元組數進行統計

  • 在計算目標物件位置時,把存活比例較高的 region 加入到不進行壓縮的 region 集合

  • 更新引用的位置與現有情況保持一致

  • 在壓縮時跳過不進行壓縮的 region 集合 為此,我們為使用者提供如下引數來使用該特性:

引數 說明
G1FullGCNoMoving 開啟選項後,當 G1 進行 Full GC 時,將會啟用此優化功能。此選項預設關閉
G1NoMovingRegionLiveBytesLowerThreshold 指定不進行壓縮的 region 中存活位元組佔比的最小值。即當 region 中的存活位元組數佔比超過設定的值時,將該 region 加入到不進行壓縮的 region 集合。預設值為 98

在存活物件佔比較高的 Full GC 中,使用此特性將會減少 Full GC 的停頓時間。

效能測試

Dacapo 是一種可以對程式語言、記憶體管理和計算機體系結構進行測試的 Java 基準測試工具,由以下套件組成:avrora、batik、eclipse、fop、h2、jython、pmd、tomcat、daytrader、xalan、lucene 等,其中 h2 是一種類似於 JDBCbench 的記憶體基準測試,針對銀行應用程式的模型執行許多事務,詳見[3]。這裡對 h2 進行測試。

測試環境:

  • CPU: Kunpeng 920

  • OS: CentOS 7.6

  • JDK: 畢昇 JDK 11.0.10

測試指令碼 script.sh 如下,執行./script.sh h2,然後即可統計 Full GC 停頓時間。

#!/bin/bash
export java=$JAVA_HOME/bin/java
export java_options="-Xmx1g -Xms1g -XX:ParallelGCThreads=4"
echo $java $java_options

task=$1
for i in `seq 30`
do
  echo ">>>>>>>>>>>>>>>>>>>> base $i <<<<<<<<<<<<<<<<<<<<<<"
  $java $java_options -Xlog:gc*=info:file=h2-base/gc-$task-base-$i.log -jar dacapo-9.12-bach.jar -t 4 --iterations 5 --size huge --no-pre-iteration-gc $task
done
for i in `seq 30`
do
  echo ">>>>>>>>>>>>>>>>>>>> opt $i <<<<<<<<<<<<<<<<<<<<<<"
  $java $java_options -XX:+G1FullGCNoMoving -Xlog:gc+phases=trace,gc=info,gc+heap=info,gc+task=info:file=h2-opt/gc-$task-opt-$i.log -jar dacapo-9.12-bach.jar -t 4 --iterations 5 --size huge --no-pre-iteration-gc $task
done

測試結果:其中 Percentile 為箱線圖中的概念,10% 即表示將資料從小到大排序,第 10%個數據提升 11.25%.

測試結果

結論:從圖中可以看到,優化之後可以將 G1 Full GC 的停頓時間降低 3%~11%。

該優化的一部分已合入 OpenJDK 社群[4],剩餘部分正在推進中。

LazyBox 特性介紹

Java 為每種基本型別提供了對應的包裝型別,將基本型別轉換為包裝型別在 Java 中稱為裝箱。由於泛型的存在,在 Java 中會頻繁的進行裝箱拆箱操作,帶來許多額外的開銷,典型例子如下:

<T extends Number>
int add(T a, T b) {
  return a.intValue() + b.intValue();
}

LazyBox 特性通過在 Hotspot C2 中推遲裝箱的時機,使 C2 只進行必要的裝箱,減少裝箱操作,提高 C2 生成程式碼的執行效率。

場景分析

對於某些裝箱後的值,在某些路徑下並不會被用到,但還是會忍受裝箱帶來的開銷。比如下面程式碼中的 integer 物件:

  int sum = 0;
  for (int i = 0; i < 100; i++) {
    Integer integer = Integer.valueOf(299);
        if (i < 1) {  //冷路徑
             blackhole.consume(integer); //逃逸
             sum+=2;
        } else {     //熱路徑
             sum+=integer.intValue(); //拆箱
        }
  }
  return sum;

當 i>=1 時,程式碼只是想獲取 integer 物件的 int 值,此時沒有必要對 integer 進行裝箱,在此中場景下,使用 LazyBox 可以極大的提高效能。只有等到確實需要裝箱時,再在 C2 中插入裝箱操作。

不過由於在每次需要裝箱時,都會插入裝箱操作,所以在某些場景下可能會導致物件不一致。例如下面的場景:

Data data = new Data();

int value = 299;
Integer a = Integer.valueOf(value);
data.a = a;
data.b = a;

System.out.println(data.a == data.b); //開啟LazyBox後為false

由於將 a 賦值給 data 中的 a  b 之前,都在 C2 中插入了裝箱操作,所以導致 data 中的 a  b 為不同的物件,所以在使用 LazyBox 時,使用者需要留心這一點。

相關引數如下:

引數 說明
LazyBox 開啟 LazyBox 特性,該選項為實驗選項,需要開啟-XX:+UnlockExperimentalVMOptions, 同時由於 LazyBox 依賴 AggressiveUnBoxing 優化,所以還需開啟-XX:+AggressiveUnboxing 選項
PrintLazyBox 列印 LazyBox 狀態,用於開發者除錯

使用者可通過如下方式使用 LazyBox 特性:

-XX:+UnlockExperimentalVMOptions -XX:+AggressiveUnboxing -XX:+LazyBox

效能測試

測試環境:

  • CPU: Kunpeng 920

  • OS: openEuler 20.03

  • JDK: 畢昇 JDK 11.0.10

本實驗採用工業級測試套件 SPECPower 進行測試,在測試過程中進行了綁核,測試結果表明:結合 畢昇 JDK 以前的優化,相比 OpenJDK 可以提升 SPECPower 10%.

鯤鵬硬體加速的 KAEProvider

KAE(Kunpeng Accelerate Engine)加解密是鯤鵬 920 處理器提供的硬體加速方案,可以顯著降低處理器消耗,提高處理器效率[5].畢昇 JDK 為 Java 使用者提供 KAEProvider,使 Java 開發人員可以直接使用硬體帶來的加速效果,提升加解密效率。

實現

Java 通過 JCA(Java Cryptography Architecture)為開發者提供了良好的介面,開發者只需要實現 SPI(Service Provider Interface)介面,並在 CSP(Cryptographic Service Provider, 下文簡稱 Provider)中進行註冊,即可讓使用者使用自己的加解密實現。舉個例子:當用戶通過MessageDigest.getInstance("SHA-256")獲取 message digest 物件時,JCA 會依次搜尋註冊的 Provider,直到找到一種實現為止,大體過程如下[6]:

使用者可通過 java.security 檔案指定各個 Provider 的優先順序,或者在程式碼中通過Security.insertProviderAt(Provider, int)介面指定.也可以在獲取摘要物件時手動指定從哪個 Provider 中獲取,如MessageDigest.getInstance("SHA-256","KAEProvider"),這種情況下,JCA 將優先使用使用者指定的 Provider.

畢昇 JDK 當前實現了 MessageDigest(MD5,SHA256,SHA384)Cipher(AES-ECB,AES-CBC,AES-CTR,RSA)KeyPairGenerator(RSA)HMac 等 SPI,並在 KAEProvider 中進行了註冊,其它演算法會在後續版本合入,使用者可通過如下方式來使用 KAEProvider.

  • 方式 1: 使用 Security API 新增 KAE Provider ,並設定其優先順序。

Security.insertProviderAt(new KAEProvider(), 1);
  • 方式 2:修改 jre/lib/security/java.security 檔案,新增 KAE Provider,並設定其優先順序。

security.provider.1=org.openEuler.security.openssl.KAEProvider
security.provider.2=sun.security.provider.Sun
security.provider.3=sun.security.rsa.SunRsaSign
security.provider.4=sun.security.ec.SunEC
security.provider.5=com.sun.net.ssl.internal.ssl.Provider
security.provider.6=com.sun.crypto.provider.SunJCE
security.provider.7=sun.security.jgss.SunProvider
security.provider.8=com.sun.security.sasl.Provider
security.provider.9=org.jcp.xml.dsig.internal.dom.XMLDSigRI
security.provider.10=sun.security.smartcardio.SunPCSC
security.provider.11=sun.security.mscapi.SunMSCAPI

效能測試

JMH(Java Microbenchmark Harness)是 OpenJDK 社群提供的一種對 Java 進行 benchmark 測試的工具,使用方式見[7].畢昇 JDK 已將對應的 JMH 測試用例合入了 openEuler 社群[8],這裡採用對應的用例進行測試。

測試環境:

  • CPU: Kunpeng 920

  • OS: openenuler 20.03

  • KAE: v1.3.10 ,下載連結見[9]

  • JDK: 畢昇 JDK 1.8.0_282

測試結果如下,可以看到,在使用 KAEProvider 後,RSA 加解密效能明顯提升。

測試結果

結論:相比 JDK 預設 Provider 提供的 RSA 加解密,當金鑰長度為 2048 位時,KAEProvider 可以提升 53%~80%,當金鑰長度為 4096 位時,KAEProvider 可以提升 70%~83%。

Jmap 併發掃描介紹

當前 jmap 採用單執行緒對 java 堆進行掃描,掃描速度較慢,並且當對超大堆進行掃描時(大於 200G),容易引起系統卡死。因此可以通過多執行緒來進行掃描,減少卡頓時間。

實現

畢昇 JDK 將社群高版本的 jmap 優化回合到此次釋出中,為 jmap -histo 選項增加指定併發執行緒數的 parallel 引數,使 jmap 可以使用多執行緒對堆進行掃描,有效提高 jmap 的掃描效率,減少掃描時間。具體實現原理可參考[10]。使用者可通過在 jmap -histo 後增加 parallel 引數來使用此特性,如下所示:

  • jmap -histo:live,parallel=3 pid : 指定併發執行緒數為 3

  • jmap -histo:live,parallel=0 pid : 使用當前系統可支援的併發執行緒數(-XX:ParallelGCThreads)

  • jmap -histo:live,parallel=1 pid : 使用原有的序列掃描

當前的實現只支援 G1 和 ParallelGC,後續版本將支援 CMS.

Bug fixes

除了上面介紹的一些特性外,畢昇 JDK 還合入了社群高版本中的一些 bug fix 和優化的 patch,為使用者提供穩定、高效能的畢昇 JDK。具體回合 patch 如下:

  • JDK8

    • 8231841: AArch64: debug.cpp help() is missing an AArch64 line for pns

    • 8254078: DataOutputStream is very slow post-disabling of Biased Locking

    • 8168996: C2 crash at postaloc.cpp

    • 8140597: Forcing an initial mark causes G1 to abort mixed collections

    • 8214418: half-closed SSLEngine status may cause application dead loop

    • 8259886: Improve SSL session cache performance and scalability

  • JDK11

    • 8254078: DataOutputStream is very slow post-disabling of Biased Locking

    • 8217918: C2: -XX:+AggressiveUnboxing is broken

 

參考

  • [1] Bishengjdk8下載:http://mirrors.huaweicloud.com/kunpeng/archive/compiler/bisheng_jdk/bisheng-jdk-8u282-linux-aarch64.tar.gz

  • [2] Bishengjdk11下載:http://mirrors.huaweicloud.com/kunpeng/archive/compiler/bisheng_jdk/bisheng-jdk-11.0.10-linux-aarch64.tar.gz

  • [3] dacapo介紹:http://dacapobench.org

  • [4] 8263495: Gather liveness info in the mark phase of G1 full gc:http://github.com/openjdk/jdk/commit/8c8d1b31

  • [5] 鯤鵬加速引擎介紹:http://support.huaweicloud.com/devg-kunpengaccel/kunpengaccel_16_0002.html

  • [6] Java Cryptography Architecture (JCA) Reference Guide:http://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html

  • [7] jmh介紹:http://github.com/openjdk/jmh

  • [8] KAEProvider jmh用例:http://gitee.com/openeuler/bishengjdk-8/tree/master/jdk/test/micro/org/openeuler/bench/security/openssl

  • [9] KAE下載連結:http://github.com/kunpengcompute/KAE/releases

  • [10] 8239290:Add parallel heap iteration for jmap -histo:http://bugs.openjdk.java.net/browse/JDK-8239290