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下載:https://mirrors.huaweicloud.com/kunpeng/archive/compiler/bisheng_jdk/bisheng-jdk-8u282-linux-aarch64.tar.gz

  • [2] Bishengjdk11下載:https://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:https://github.com/openjdk/jdk/commit/8c8d1b31

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

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

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

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

  • [9] KAE下載鏈接:https://github.com/kunpengcompute/KAE/releases

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