📔

Head toward Java 16 (Night Seminar Edition)

2021/03/26に公開

Head toward Java 16 (Night Seminar Edition)

https://www.slideshare.net/YujiKubota/head-toward-java-16-night-seminar-edition

KUBOTA Yuji
SBI Security Solutions

JJUG Night Seminar (2021/Mar/16)


KUBOTA Yuji (@sugarlife)


今日話すこと

  • Java 16の新機能と変更点
    • 主にAPI以外のVM機能

話さないこと


References


Glossary (1/2)

JEP (JDK Enhancement-Proposal)

JEP 1で定義されているJava新機能拡張の提案

CSR (Compatibility & Specification Reviews)

非互換性を伴う変更のレビュー(非互換性を伴う変更は出す必要がある)

Release note
各製品ごとの変更点。OpenJDKやOracle JDKで書かれている内容がすべての製品にあてはまるわけではないのことに注意


Glossary (2/2)

Incubator (JEP 11)
Java APIの試験用モジュール(jdk.incubator)。標準化に向けてフィードバックを得て変更しやすいように特別なモジュールにしている
有効にするには--add-modules jdk.incubator.xxxの指定が必要

Preview (JEP 12)
Java言語やJVMの試験機能。有効にするには--enable-previewの指定が必要。コンパイル時には--resource-sourceの指定も必要

Standard
上記のテストフェーズを通じて標準機能に昇格した機能。通例は2回(例:Preview -> Second Preview -> Standard)で昇格しているが、Foreign-Memory Access APIのように3回目を迎えるのもある


Java 16


Ecosystem changes


347: Enable C++14 Language Features

目的: C++14機能を利用して効率よくJDKを実装できるようにする
背景: JDKやJVMはC++で実装されているが機能はC++98/03に長らく限定されていた。JDK 11から新しいバージョンでの「ビルド」はサポートされ始めたが、言語機能の利用自体は許可されていなかった

ちなみにHotSpot(JVM実装)とJDK実装ではこのルールが異なり、HotSpotの実装ではビルドルールでC++ exceptionsは利用できないようにされている

Java利用者にとっては特に影響なし


357: Migrate from Mercurial to Git

目的: OpenJDKをGitで管理する
背景: 長らくOpenJDKはMercurial(hg)で管理されていたが開発者との親和性などの観点からGitに移行を図った(使いづらい、開発者を呼び込みたい、など)

バージョン管理やコミットメッセージのリフォーマット、各種チェッカー(jcheck)やレビューツール(webrev)などの移植が行われた]

既存Mercurialリポジトリ(hg.openjdk.java.net)は積極的に廃棄されず残る予定


369: Migrate to GitHub

目的: OpenJDKをGitHubで管理する
背景: 357と一緒。こちらは構成管理をどのプラットフォームで行うか

リポジトリは https://github.com/openjdk/
今までjava.netにあったレビューツールやチェッカーなどもGitHubエコシステムに移植

Bylaws(グループやプロジェクト、開発者のロール、ガバメントボードなどの原則)Census(グループやプロジェクト、開発者、およびその関連性)は変わらない
課題管理やWikiなどのインフラも変わらずjava.net上のまま(いつでもGitHubから移行できるように=影響を受けないように保つという目的もある)


Language/API changes


380: Unix-Domain Socket Channels (1/4)

目的: 単一マシン上でのプロセス間通信に使用されるUnix Domain Socket(AF_UNIX)を標準APIでサポート
背景: プロセス間通信はTCP/IP(ループバック接続)よりも効率的でセキュアな通信が行え、近年Windowsでサポートされ始めた。これにより主要プラットフォームでサポートされたので標準APIに入った。同一システム上のコンテナ間通信でも、共有ボリュームを利用することでUnix-Domain Socket利用が実現できる

効率的: Unix-Domain Socketはファイルシステムのパス名(C:¥hoge, /hoge)を指定して通信する。つまり、ループバックTCP/IPソケットではない=TCP/IPスタックを通らないのでLatencyやCPU使用率が改善できる
セキュア: ①ローカルアクセスは必要だがリモートネットワークアクセスが不要なのでリモートアクセスを遮断できる②プラットフォーム(Unix, Windows)のファイルシステムのアクセス制御を適用できる


380: Unix-Domain Socket Channels (2/4)

制約: ①UnixではサポートされているがWindowsでサポートされていない機能はこのJEPで対応することはゴールとして設定されていない。今後JDKごとの固有オプションとして対応される可能性がある。例えばPeer credentialsはUnix固有だが、ソケットオプションSO_PEERCREDがすでに対応されている。②レガシーAPI (java.net.Socket, java.net.ServerSocket) のサポートは行われていない。これはTCP/IP (java.net.InetAddress) を前提に実装されているため


380: Unix-Domain Socket Channels (3/4)

import java.net.UnixDomainSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;

import static java.net.StandardProtocolFamily.UNIX;

public class Server {
    private static final Path SOCKETE_FILEPATH = Path.of("./", "testsocket");
    public static void main(String... args) throws Exception {
        var address = UnixDomainSocketAddress.of(SOCKETE_FILEPATH);
        try (var serverChannel = ServerSocketChannel.open(UNIX)) { // INETかUNIXかを指定
            serverChannel.bind(address);
            try (var clientChannel = serverChannel.accept()) {
                ByteBuffer buf = ByteBuffer.allocate(64);
                clientChannel.read(buf);
                buf.flip();
                System.out.printf("Read %d bytes\n", buf.remaining());
            }
        } finally {
            Files.deleteIfExists(address.getPath()); // ソケットと独立してファイルが存在するので確実に消す
        }
    }
}

380: Unix-Domain Socket Channels (4/4)

import java.net.UnixDomainSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.file.Path;

public class Client {
    private static final Path SOCKETE_FILEPATH = Path.of("./", "testsocket");
    public static void main(String... args) throws Exception {
        var address = UnixDomainSocketAddress.of(SOCKETE_FILEPATH);
        try (var clientChannel = SocketChannel.open(address)) {
            ByteBuffer buf = ByteBuffer.wrap((args.length != 0 ? args[0] : "Hello world").getBytes());
            clientChannel.write(buf);
        }
    }
}

ref: https://inside.java/2021/02/03/jep380-unix-domain-sockets-channels/


389: Foreign Linker API (Incubator) (1/2)

目的: Project Panama。ネイティブ実装をもっと楽に呼び出したい
背景: JNIめんどい

  1. Nativeを呼び出すJava実装を書く private static native int getpid()
  2. headerファイルを作る javac -h . XXX.java
  3. JNI APIを使ってC実装を書く JNIEXPORT jint JNICALL Java_XXX_getpid(JNIEnv *env, jclass clas){...}
  4. コンパイル cc -shared -o libmain.dylib -I $JAVA_HOME/include -I XXX.c
  5. Java実装に動的ライブラリを読み込むコードを書くSystem.loadLibrary("XXX");

389: Foreign Linker API (Incubator) (2/2)

import java.lang.invoke.*;
import jdk.incubator.foreign.*;

class PanamaMain {
  public static void main(String[] args) throws Throwable {
    // System Linker取得、ネイティブシンボルのルックアップ
    var linker = CLinker.getInstance();
    var lookup = LibraryLookup.ofDefault();

    // C宣言のメソッドハンドル作成、関数記述子算出
    var getpid = linker.downcallHandle(
           lookup.lookup("getpid").get(),
           MethodType.methodType(int.class),
           FunctionDescriptor.of(CLinker.C_INT));

    // 実行
    System.out.println((int)getpid.invokeExact());
  }
}
$ java -Dforeign.restricted=permit --add-modules jdk.incubator.foreign  PanamaMain.java

ref. https://inside.java/2020/10/06/jextract/
より楽に: C headerファイルから上記相当の処理を行うJavaクラスを jextract で作成できる (サンプル集)


390: Warnings for Value-Based Classes (1/4)

目的: Value-Based Classesの利用方法を制限する
背景:

  • Project Valhallaはprimitive classを活用してインライン表現が可能なようにプログラミングモデルを強化する
  • JDK 8からある"Value-Based Classes(以降、値クラス)"もこの対象にしたい
    1. 不変(可変オブジェクトへの参照は可)
    2. インスタンス状態からのみ計算されるequals, hashCode, toString実装を持つ
    3. インスタンスIDに依存した操作を使用しない(synchronized, ==, hashCode)
    4. equals()のみに基づいて同等とみなす(==を利用した参照に基づく等価判定を行わない)
    5. 公開されたコンストラクタを持たない(一意性に寄与しないファクトリメソッドで作成される)
    6. 同等な時は自由に置き換え可。インスタンスxとyが示す「値」が同じ時は入れ替えても変化なし
  • 対象にするには以下のリスクが考えられる
    1. !=の結果に依存しているコードが破壊される
    2. コンストラクタ経由でIntegerのようなpritimive wrapper classのインスタンスを作っている場合は LinkageErrors が発生する
    3. これらのクラスのインスタンスを同期処理 (synchronized) させようとした場合例外が発生する
  • というわけで警告を出して利用方法を制約しておきたい←本旨

390: Warnings for Value-Based Classes (2/4)

i. については、値クラスはFactoryメソッドで一意性の保証はしておらず、仕様を無視した前提のコードは自動的に検出も難しいのでスコープ外とする

ii. については、Java 9で既に非推奨化されておりコンストラクタが呼ばれていればデフォルトでコンパイル時(javac)に警告が出される

iii. については、値クラスを対象にsynchronized文を利用していた場合はコンパイル時に警告が出るようになった
また、実行時にも値クラスのインスタンス上でのmonitorenterが検出された場合、-XX:DiagnoseSyncOnValueBasedClasses1を設定していた場合はfatalエラーとして扱い、2を設定した場合は警告がコンソールおよびJFRのイベント経由でロギングされるようになった(注:UnlockDiagnosticVMOptionsの設定も必要)


390: Warnings for Value-Based Classes (3/4)

$ ./jdk-16.jdk/bin/javac ValueBased.java
ValueBased.java:5: 警告:[synchronization] 値ベース・クラスのインスタンスで同期しようとしました
    synchronized (valueBased)) {
    ^
警告1個
$ ./jdk-16.jdk/bin/java -XX:+UnlockDiagnosticVMOptions -XX:DiagnoseSyncOnValueBasedClasses=1 ValueBased
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (synchronizer.cpp:416), pid=71724, tid=11267
#  fatal error: Synchronizing on object 0x00000007bfe78198 of klass java.lang.Long at ValueBased.main(ValueBased.java:5)
:(snip)
$ ./jdk-16.jdk/bin/java -XX:+UnlockDiagnosticVMOptions -XX:DiagnoseSyncOnValueBasedClasses=2 ValueBased
[0.101s][info][valuebasedclasses] Synchronizing on object 0x00000007bfe78198 of klass java.lang.Long
[0.101s][info][valuebasedclasses]       at ValueBased.main(ValueBased.java:5)
[0.101s][info][valuebasedclasses]       - locked <0x00000007bfe78198> (a java.lang.Long)
:(snip)

390: Warnings for Value-Based Classes (4/4)

実現するために、対象クラスに@jdk.internal.ValueBasedアノテーション追加

  • java.lang.{Integer,Long,Short,...} (primitive wrapper class)
  • java.lang.Runtime.Version
  • java.util.{Optional,OptionalInt,OptionalLong,OptionalDouble}
  • java.time.{Instant,LocalDate,LocalTime,...}, java.time.chrono.MinguoDate,HijrahDate,...
  • java.lang.ProcessHandle (+実装クラス)
  • java.utilのcollectionファクトリー実装クラス
    • java.util.List.{of,copyOf}
    • java.util.Map.{of,copyOf,ofEntries,entry}

396: Strongly Encapsulate JDK Internals by Default

目的: sun.misc.Unsafeを含む重要なAPIを除くJDK内部実装へのアクセスを原則不可
背景: JDK9で実施されたモジュール化に伴い、外部利用を意図していない内部実装へのアクセスを不可にしてメンテナンスコストを下げたい。しかし、代替不可能かつ依存しているライブラリが多いsun.misc.Unsafeのような内部実装もあり、一律して全てを不可にすることは難しいという状態が続いている

JEP 261から導入された--illegal-accessのデフォルト値がpermit(最初のリフレクティブアクセス時に警告がでるが原則許可)からdeny(--add-opensオプションなどを用いて明示的に許可してない限りアクセス不可)へ変更。jdepsコマンドを使って依存モジュールを確認して適切にアクセス設定を行いましょう


JVM changes


376: ZGC: Concurrent Thread-Stack Processing (1/3)

目的: スレッドスタック処理をsafepointから並行フェーズに移行することで、ZGCのSTW(Stop The World)を更に減らす
背景: 次ページ
利点: ZGCを使っている場合、何もせず高速化する可能性がある


376: ZGC: Concurrent Thread-Stack Processing (2/3)

背景(1): GCがなぜSTWを必要としているか

  • GCは各ヒープオブジェクトへのポインタに関する情報が必要
  • これはJITコンパイラによって作成されたオブジェクトマップ(OopMap)に含まれる
  • JITによってコンパイルされた各メソッドは、特定の場所にOopMapを記録し、スタックとレジスタのどの場所が参照されてるかを記録する
  • GCは、スタックをスキャンするときにこれらのOopMapを照会して、参照するポインタがどこにあるかを把握する
  • GC時に各スレッドがポインタを触っていると予期せぬ領域に対して処理が走る可能性があるので、そのような(unsafeな)スレッドはサスペンドする必要がある
  • 各スレッドにsafepointに関する状態と遷移時の処理を持たせてlockすることで、GCなどのVMThread処理(vmoperation)を行う際に各スレッドをblockする(そしてVMThreadがsafepointにて処理を行う)
  • 結果的に各Javaスレッドが止まってる状態なので"vmoperation ≒ safepoint ≒ STW"となっている



376: ZGC: Concurrent Thread-Stack Processing (3/3)

背景(2): ZGCにおけるSTW

  • 元々ZGCはマーキング処理などヒープサイズやメタスペースサイズに応じて増大化するGC処理を、safepointでの操作から追い出して並行フェーズ(JavaスレッドとGCスレッドを並行に処理)に移行させることでヒープサイズに関わらず1ミリ秒のSTWを達成してきた
  • ルート処理などの一部がまだsafepointで行われている。ルートオブジェクト数はヒープサイズ依存ではないが、主にスレッド数などに依存するので大規模アプリでは問題になりえる
  • stack watermark barrierを利用してGCがすべてのスレッドスタックと他のスレッドルートを処理をしている際にJavaスレッドがフレームに戻らないように整合性を確保することで並列化を実現
  • 将来的に他GCへの転用が望める


387: Elastic Metaspace

目的: Metaspaceのフラグメンテーションやメモリフットプリントなどを改善

  • 背景: Metaspaceはヒープ外メモリを多く消費するケースがある
    • メタデータは通常、クラスがロードされたときに確保され、その生存期間はクラスローダーに依存する
    • Metaspaceのチャンクはアロケーション操作を効率的に行うため粒度が粗い
    • クラスローダが回収されるとMetaspaceのチャンクは後で再利用するためにフリーリストに配置される
    • 結果、頻繁にクラスローディングとアンローディングを行うアプリケーションは、大量の使わないスペースをMetaspaceのフリーリスト内に蓄積してしまう→ここをどうにかしたい
  • 方法: メモリアロケータをKernelなどでも利用されているbuddy allocationに置き換え
    • より小さなチャンクでメタスペースメモリを割り当てられるようになり、クラスローダのオーバーヘッドを減らせる
    • 同様にフラグメンテーションが減らせ、未使用のメタスペースメモリをより迅速にOSに戻せるようになる(※元々断片化されてなかったらOSに返す)
  • 利点: 何もせずにJavaアプリケーションのメモリ使用量が改善

ref. https://cr.openjdk.java.net/~stuefe/JEP-Improve-Metaspace-Allocator/review-guide/review-guide-1.0.html


Tool changes


392: Packaging Tool

目的: JDK14で入ったjpackage (JEP 343)が標準機能化
背景: 各プラットフォーム間で共通のインストーラー生成ツールがなかったので作られた。これからは jlink でカスタムランタイムイメージを作り、jpackage でインストーラーを作ってユーザに提供する流れが一般的に(なるかもしれない)

JDK14からの変更点は--bind-servicesオプションが--jlink-optionsに変わっただけ


Basic usage

$ jpackage --name sample --input <directory_includes_jar> \
  --main-jar sample.jar --main-class Sample
$ ls sample-*
sample-1.0.dmg

<!--
--inputでjarファイルが置かれているディレクトリを指定し、メインJARファイル、メインクラスをそれぞれ指定して実行する。
すると、macOSであればsample-1.0.dmgなど、OSに合わせたインストーラが作成される。
-->

With custom runtime image:
<!--カスタムイメージでの作り方は
依存しているモジュールを表示する-->

# Show ependent modules
$ jdeps -summary Sample.class
Sample.class -> java.base
Sample.class -> java.desktop
# Create runtime image that consists only of dependent modules
$ jlink --add-modules java.base,java.desktop --output image
# Create installer with custom runtime image
$ jpackage --name sample --input <directory_includes_jar> \ 
  --main-jar sample.jar --main-class Sample --runtime-image image

<!--
jlinkで作ったカスタムイメージを--runtime-imageで指定しているだけ。この方法を利用するメリットはインストールされるサイズが抑えられることと、インストーラそのもののファイルサイズも抑えられるところ。

JavaはRun anywhereを掲げておりJARファイルの提供だけで済むのが言語のメリットの一つではあるが、Javaの実行バイナリがより多くのベンダや団体から提供されるようになったので、このようなインストーラでバイナリごとインストールさせるメリットが以前より大きくなったと言える。
-->


Head toward Java 16 (Night Seminar Edition)

KUBOTA Yuji
SBI Security Solutions

JJUG Night Seminar (2021/Mar/16)

Discussion