Head toward Java 16 (Night Seminar Edition)
Head toward Java 16 (Night Seminar Edition)
KUBOTA Yuji
SBI Security Solutions
JJUG Night Seminar (2021/Mar/16)
@sugarlife)
KUBOTA Yuji (- Manager of AWS infrastructure team at SBI Security Solutions
- We're hiring!
- https://www.sbisecsol.com/
- OpenJDK Author / IcedTea committer
- JavaOne 2014/2016/2017, WEB+DB「Dive to Java」(2018-19)
- https://www.slideshare.net/YujiKubota/
今日話すこと
- Java 16の新機能と変更点
- 主にAPI以外のVM機能
話さないこと
References
- bugs.openjdk.java.net (通称 JBS, b.o.j.n)
- https://github.com/openjdk/jdk
Glossary (1/2)
JEP (JDK Enhancement-Proposal)
JEP 1で定義されているJava新機能拡張の提案
CSR (Compatibility & Specification Reviews)
非互換性を伴う変更のレビュー(非互換性を伴う変更は出す必要がある)
Release note
各製品ごとの変更点。OpenJDKやOracle JDKで書かれている内容がすべての製品にあてはまるわけではないのことに注意
- OpenJDK: JBS
- Oracle JDK: https://www.oracle.com/java/technologies/javase/jdk-relnotes-index.html
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
- 357: Migrate from Mercurial to Git
- 369: Migrate to GitHub
- 386: Alpine Linux Port @kisさん担当、以下@kisと記載
- 388: Windows/AArch64 Port @kis
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
- 338: Vector API (Incubator) @kis
- 380: Unix-Domain Socket Channels
- 389: Foreign Linker API (Incubator)
- 390: Warnings for Value-Based Classes
- 393: Foreign-Memory Access API (Third Incubator) @kis
- 394: Pattern Matching for instanceof @kis
- 395: Records @kis
- 396: Strongly Encapsulate JDK Internals by Default
- 397: Sealed Classes (Second Preview) @kis
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めんどい
- Nativeを呼び出すJava実装を書く
private static native int getpid()
- headerファイルを作る
javac -h . XXX.java
- JNI APIを使ってC実装を書く
JNIEXPORT jint JNICALL Java_XXX_getpid(JNIEnv *env, jclass clas){...}
- コンパイル
cc -shared -o libmain.dylib -I $JAVA_HOME/include -I XXX.c
- 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(以降、値クラス)"もこの対象にしたい
- 不変(可変オブジェクトへの参照は可)
- インスタンス状態からのみ計算される
equals
,hashCode
,toString
実装を持つ - インスタンスIDに依存した操作を使用しない(
synchronized
,==
,hashCode
) -
equals()
のみに基づいて同等とみなす(==
を利用した参照に基づく等価判定を行わない) - 公開されたコンストラクタを持たない(一意性に寄与しないファクトリメソッドで作成される)
- 同等な時は自由に置き換え可。インスタンスxとyが示す「値」が同じ時は入れ替えても変化なし
- 対象にするには以下のリスクが考えられる
-
!=
の結果に依存しているコードが破壊される - コンストラクタ経由で
Integer
のようなpritimive wrapper classのインスタンスを作っている場合はLinkageErrors
が発生する - これらのクラスのインスタンスを同期処理 (
synchronized
) させようとした場合例外が発生する
-
- というわけで警告を出して利用方法を制約しておきたい←本旨
390: Warnings for Value-Based Classes (2/4)
i. については、値クラスはFactoryメソッドで一意性の保証はしておらず、仕様を無視した前提のコードは自動的に検出も難しいのでスコープ外とする
ii. については、Java 9で既に非推奨化されておりコンストラクタが呼ばれていればデフォルトでコンパイル時(javac
)に警告が出される
iii. については、値クラスを対象にsynchronized
文を利用していた場合はコンパイル時に警告が出るようになった
また、実行時にも値クラスのインスタンス上でのmonitorenter
が検出された場合、-XX:DiagnoseSyncOnValueBasedClasses
に1
を設定していた場合は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アプリケーションのメモリ使用量が改善
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