Java 16が2021/3/16にリリースされました。
多くのプログラマに関係ありそうな変更は、record、instanceofパターンマッチの正式化、Stream.toListです。またTLS1.0と1.1が無効になっているので古いMySQLなどへの接続でハマることがあるかもしれません。
詳細はこちら
Java SE 16 Platform JSR 391
JDK 16 GA Release
APIドキュメントはこちら
Overview (Java SE 16)
MacやLinuxでのインストールにはSDKMAN!をお勧めします
Oracle OpenJDK以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。
アップデートは4月に16.0.1が、翌年7月に16.0.2がリリースされることになります。
Oracle JDKは開発用途には無償利用できますが、商用利用にはJava SE Subscriptionを購入するなどのライセンスが必要になります。
JEP
大きめの変更はJEPでまとまっています。
http://openjdk.java.net/projects/jdk/16/
今回は17個のJEPが取り込まれました。すでにプレビューなどで出ていたものが5つあり、新たに取り込まれたものは12です。
JEP 338: Vector API (Incubator)
JEP 347: Enable C++14 Language Features
JEP 357: Migrate from Mercurial to Git
JEP 369: Migrate to GitHub
JEP 376: ZGC: Concurrent Thread-Stack Processing
JEP 380: Unix-Domain Socket Channels
JEP 386: Alpine Linux Port
JEP 387: Elastic Metaspace
JEP 388: Windows/AArch64 Port
JEP 389: Foreign Linker API (Incubator)
JEP 390: Warnings for Value-Based Classes
JEP 392: Packaging Tool
JEP 393: Foreign-Memory Access API (Third Incubator)
JEP 394: Pattern Matching for instanceof
JEP 395: Records
JEP 396: Strongly Encapsulate JDK Internals by Default
JEP 397: Sealed Classes (Second Preview)
分野ごとにまとめていきます。
言語機能
言語仕様にかかわる変更としては次のようなものがあります。
Pattern Matching for instanceofとRecordsは正式化されました。
プレビューで残ってるのはSealed Classesだけですね。これも17LTSでは正式化されるはずで、プレビュー機能を残さないようにしているのだと思います。
JEP 394: Pattern Matching for instanceof
JEP 395: Records
JEP 397: Sealed Classes (Second Preview)
JEP 390: Warnings for Value-Based Classes
Inner ClassでStaticメンバーの定義
RecordがInner Classで定義できなかったので、その原因になるstaticメンバーの制限が緩和されています。
public class Outer {
class Inner {
static final String MESSAGE = "hoge";
static String message() {
return MESSAGE;
}
}
}
JEP 394: Pattern Matching for instanceof
パターンマッチングです。
instanceofを使ったパターンマッチが14でプレビューとして入り、そのまま変更なしにJava 15でセカンドプレビューになりました。
Java 16では、パターン変数が変更可能になりました。これはローカル変数との非対称性を減らすためということです。
また、いままでString型のsに対してs instanceof Object o
のような常に真となるマッチも可能でしたが、Java 16からはコンパイルエラーになります。
jshell> var s = ""
s ==> ""
jshell> s instanceof Object o
| エラー:
| パターン・タイプjava.lang.Objectは式タイプjava.lang.Stringのサブタイプです
| s instanceof Object o
| ^--
値 instanceof パターン
で、値をマッチさせることができます。
パターンは、定数か変数定義です。変数定義の場合には、型が一致していた場合にtrueになりその変数に値が割り当てられます。
if (x instanceof Integer i) {
// can use i here
}
switchで使えるようになれば便利ですが、これは別のJEPで定義されていて、Java 17以降に持ち越されています。17LTSにはプレビュー機能は入らないと思うので、来年3月のJava 18ですかね。
JEP draft: Pattern matching for switch (Preview)
今後は、Recordsと組み合わせてrecord Point(int x, int y){}
があるときにp instanceof(var x, var y)
のような分解のパターンも使えるようにするようです。
また、Optional.of
のようなビルダーに対して同様にパターンを使えることも考えてるようです。つまりos instanceof Optional.of(String s)
のようなパターンが書けるようにするということです。
楽しみですね。
https://github.com/openjdk/amber-docs/blob/master/site/design-notes/pattern-match-object-model.md
分解のパターンについてはこちら。
http://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html
JEP 395: Records
レコードが標準化される
Java 14でJEP 359としてPreviewになりJava 15でJEP 384として少し変更を加えてSecond Previewになっていました。
Java 16では、インナークラス内でも定義可能にして標準化されました。
定義
recordとして定義します。
record Foo(int x, int y) {}
他のクラスは継承できません。
record レコード名(状態) { 定義 }
これは次のようなクラスになります。
class Foo extends Record {
// 状態がprivate finalとして定義
private final int x;
private final int y;
// 追加のインスタンスフィールドは定義できない
// 状態をフィールドに設定するコンストラクタが定義される
public Foo(int x, int y) {
this.x = x;
this.y = y;
}
// 状態と同じ名前のメソッドが定義される
public int x() {
return x;
}
public int y() {
return y;
}
// 状態を反映するhashCodeが定義される
public int hashCode() { ... }
// 状態を比較するequals が定義される
public boolean equals() { ... }
// 状態を表示するtoStringが定義される
public String toString() { ... }
}
取得用メソッドは定義されますが、値設定用のメソッドは定義されません。つまり、イミュータブルなオブジェクトとなります。また、get/setではないことからJava Beanとの互換性もありません。
hashCode()
メソッドやequals()
メソッドなどは実際にはinvokeDynamicで実行時に実装コードが生成されます。
次のように、状態の検査や正規化にコンストラクタが定義できます。コンストラクタの引数リストは省略できます。
record Range(int lo, int hi) {
Range {
if (lo < 0) lo = 0; // 正規化
if (lo > hi) throw IllegalArgumentException();
// 設定されなかったフィールドはあとで設定される
}
}
また、コンストラクタの引数リストを省略した場合、コンポーネントフィールドに値を割り当てるとコンパイルエラーになります。
record Range(int lo, int hi) {
Range {
this.lo = 3; // コンパイルエラー
}
}
Java 15までではstaticではないinner classの中でrecordを定義することはできませんでしたが、この制約が今回のJava 16で外れました。
public class NestedRecord {
class Foo {
record Bar(int x){}
}
}
また、Java 16ではコンポーネント定義にC形式の配列宣言が使えなくなっています。
jshell> record T(int a[]){}
| エラー:
| 旧式の配列表記法はレコード・コンポーネントでは使用できません
| record T(int a[]){}
APIの拡張
recordはjava.lang.Record
を継承したクラスになります。
次のようにRecordを継承するコードを書こうとするとrecords cannot directly extend java.lang.Record
というエラーになります。
class Foo extends Record {
}
Class
クラスにはレコード関連のメソッドが追加されています。
isRecord
メソッドで型がrecordかどうか判定できます。またgetRecordComponents
でrecordで定義されたコンポーネントを取得することができます。ただし、値の取得はRecordComponent経由では行えないので、Field経由で行うようです。
jshell> Foo.class.isRecord()
$9 ==> true
jshell> Foo.class.getRecordComponents()
$10 ==> RecordComponent[2] { int x, int y }
jshell> String.class.isRecord()
$11 ==> false
jshell> String.class.getRecordComponents()
$12 ==> null
型名としてのrecord
の制限
record
という名前を型(クラス・インタフェース・レコード型など)につけることは制限されています。
$ jshell
| JShellへようこそ -- バージョン16
| 概要については、次を入力してください: /help intro
jshell> class record{}
| エラー:
| ここでは'record'は許可されません
| リリース14から'record'は制限された型名であり、型の宣言に使用できません
| class record{}
| ^
jshell> String record=""
record ==> ""
jshell> record Rec(int record){}
jshell> Rec record=new Rec(3)
record ==> Rec[record=3]
enumと違いキーワードではないので、変数名やフィールド、recordのコンポーネント名にはrecordを使えます。
JEP 397: Sealed Classes (Second Preview)
Sealedクラスは、継承できるクラスを限定する機能です。仕様の名前は「クラス」 となってますが、interfaceでも使えます。
クラス名のあとに permits
で継承クラスを指定していくという感じになります。
public abstract sealed class Shape
permits Circle, Rectangle, Square {...}
そうするとShapeクラスを継承できるのはCircle、Rectangle、Squareに限定されます。Circle、Rectangle、Squareは必ず定義する必要があります。
これらのクラスは、モジュール化しているのであれば同じモジュール内、モジュール化していないのであれば同じパッケージ内である必要があります。
また、permitsされるクラスが同一のソースだけにあるのであれば、permitsは省略できます。
permitsするクラスは定義のときにsealedかnon-sealedかfinalをつける必要があります。recordは暗黙的にfinalになるのでなにもつける必要はありません。
現状ではほとんど使いどころがないです。
例えば、Shapeの継承先は確定しているので、次のコードはコンパイル通ってほしいところですが、エラーになります。
String getName(Shape s) {
if (s instanceof Circle) {
return "円";
} else if (s instanceof Rectangle) {
return "四角";
} else if (s instanceof Square) {
return "正方形";
}
}
まだドラフトですがPattern matching for switch (Preview)が導入されてswitch式/文でパターンマッチが使えるようになれば、次のようにdefaultなしで書ける予定です。
String getName(Shape s) {
switch (s) {
case Circle c -> return "円";
case Rectangle r -> return "四角";
case Square q -> return "正方形";
}
}
もちろん、ここでswitch式を使うこともできますね。
String getName(Shape s) {
return switch (s) {
case Circle c -> "円";
case Rectangle r -> "四角";
case Square q -> "正方形";
}
}
ということで、現時点でのSealedクラスは今後の拡張のための下準備というところです。
背景にある考え方は、次の記事で言語設計者であるBrian Goetzが解説しています。
Java 注目の機能:Sealed クラス
JEP 390: Warnings for Value-Based Classes
Integerなどvalue-based classをsynchronizedのロックに使っているとコンパイル時・実行時に警告がでる
jshell> synchronized (LocalDateTime.now()){}
| 警告:
| 値ベース・クラスのインスタンスで同期しようとしました
| synchronized (LocalDateTime.now()){}
| ^----------------------------------^
API
APIの変更に関するJEPは次のようなものがあります。
JEP 380: Unix-Domain Socket Channels
JEP 338: Vector API (Incubator)
JEP 389: Foreign Linker API (Incubator)
JEP 393: Foreign-Memory Access API (Third Incubator)
JEP 396: Strongly Encapsulate JDK Internals by Default
[JDK-8180352] Add Stream.toList() method - Java Bug System
Stream.mapMulti
[JDK-8254876] (fs) NullPointerException not thrown when first argument to Path.of or Paths.get is null - Java Bug System
APIの変更はこちらにまとまっています。
https://cr.openjdk.java.net/~iris/se/16/latestSpec/apidiffs/overview-summary.html
JEP 380: Unix-Domain Socket Channels
SocketChannelやServerSocketChannelでUNIXドメインに対応する。
同じシステム内でのプロセス間通信でTCP/IPよりも効率がよく、システムをまたがって通信しないのであればセキュアになる。
同一マシン上のコンテナ間でも共有ボリューム経由で効率的な通信ができる。
JEP 338: Vector API (Incubator)
AVX命令のような、複数のデータに対する計算を同時に行う命令をJavaから利用できるようになります。
Project Panamaのひとつとして開発されていました。
今回インキュベータとしてJava 16に導入されました。
使うためには実行時やコンパイル時に--add-modules jdk.incubator.vector
をつける必要があります。
import jdk.incubator.vector.*;
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;
void vectorComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i += SPECIES.length()) { // SPECIES.length() = 256bit / 32bit -> 8
VectorMask<Float> m = SPECIES.indexInRange(i, a.length); // 端数がマスクされる
// a.lengthが11でiが8のとき最初の3つしか要素がないので [TTT.....]
// FloatVector va, vb, vc;
FloatVector va = FloatVector.fromArray(SPECIES, a, i, m);
FloatVector vb = FloatVector.fromArray(SPECIES, b, i, m);
FloatVector vc = va.mul(va).
add(vb.mul(vb)).
neg();
vc.intoArray(c, i, m);
}
}
利用できるのは次の6つの型です。それぞれに対応するVector型があって、これが基本になります。
型 | bit幅 | Vector |
---|---|---|
byte | 8 | ByteVector |
short | 16 | ShortVector |
int | 32 | IntVector |
long | 64 | LongVector |
float | 32 | FloatVector |
double | 64 | DoubleVector |
ただ、利用するにはVectorSpeciesが必要です。利用したいVectorにSPECIES_*という定数が用意されているので、それを使います。*は一度に計算するbit数ですね。
jshell> FloatVector.SP
SPECIES_128 SPECIES_256 SPECIES_512 SPECIES_64 SPECIES_MAX SPECIES_PREFERRED
MAXではそのハードウェアで使える最大、PREFERREDは推奨ビット数だけど、同じになるんじゃないのかな。ここでは256bitが推奨されて、floatが8個同時に計算できるようになっていますね。
jshell> FloatVector.SPECIES_PREFERRED
$11 ==> Species[float, 8, S_256_BIT]
ハードウェアで使えるbit数は搭載CPUに依存しますが、普通のIntel/AMDであれば256、XEONとか つよつよCPUなら512かな。M1は128でした。ハードウェアでサポートされないbit数を使おうとするとソフトウェア処理になるので遅くなります。
実際のVectorはfrom*というメソッドで取得します。fromArray、fromByteArray、fromByteBufferが用意されています。インキュベータに入る前はfromValuesがあったのですが、なくなってますね。
Vectorを得られたら、用意されたメソッドで計算します。ひととおりの算術命令はあります。
jshell> va.
abs() add( addIndex( bitSize() blend(
broadcast( byteSize() compare( div( elementSize()
elementType() eq( equals( fma( getClass()
hashCode() intoArray( intoByteArray( intoByteBuffer( lane(
lanewise( length() lt( max( min(
mul( neg() notify() notifyAll() pow(
rearrange( reduceLanes( reduceLanesToLong( reinterpretAsBytes() reinterpretShape(
selectFrom( shape() slice( sqrt() sub(
test( toArray() toDoubleArray() toIntArray() toLongArray()
toShuffle() toString() unslice( viewAsFloatingLanes() viewAsIntegralLanes()
wait( withLane(
ところで、こういったメソッド呼び出しの内部でAVX命令などを呼び出すのでは遅くなるんではという気がしますが、実際にはJVM intrinsicsという仕組みでJITコンパイラがこれらのメソッド呼び出しをネイティブ関数呼び出しに置き換えます。
JEP 393: Foreign-Memory Access API (Third Incubator)
変更点
MemorySegmentとMemoryAddressの明確な分離
MemoryAccessインタフェースの導入。VarHandle APIの利用を最小限にする
共有セグメントのサポート
Cleanerつきセグメント
ヒープ外のメモリをアクセスする方法としては、ByteBufferを使う方法やUnsafeを使う方法、JNIを使う方法がありますが、それぞれ一長一短があります。
ByteBufferでdirect bufferを使う場合、intで扱える範囲の2GBまでに制限されたり、メモリの解放がGCに依存したりします。
Unsafeの場合は、性能もいいのですが、名前が示すとおり安全ではなく、解放済みのメモリにアクセスすればJVMがクラッシュします。
JNIを使うとCコードを書く必要があり、性能もよくないです。
ということで、ヒープ外のメモリを直接扱うAPIがJava 14でインキュベータモジュールとして導入されたわけです。そしてJava 15でセカンドインキュベータになっています。
次のようなコードになります。
import jdk.incubator.foreign.*;
import java.nio.ByteOrder;
VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
for (int i = 0 ; i < 25 ; i++) {
intHandle.set(seg, i * 4, i);
}
}
コンパイル、実行では--add-modules jdk.incubator.foreign
を付けてモジュールを読み込む必要があります。
パッケージもモジュール名と同様、jdk.incubator.foreign
になっています。
MemoryHandlesからwithOffsetとwithStrideが消えましたね。
jshell> import jdk.incubator.foreign.*
jshell> MemoryHandles.
asAddressVarHandle( asUnsigned( class collectCoordinates( dropCoordinates(
filterCoordinates( filterValue( insertCoordinates( permuteCoordinates( varHandle(
Java 15ではMemoryHandlesからVarHandleを得るメソッドが拡充してました。
jshell> MemoryHandles.
asAddressVarHandle( asUnsigned( class collectCoordinates( dropCoordinates(
filterCoordinates( filterValue( insertCoordinates( permuteCoordinates( varHandle(
withOffset( withStride(
Java 14では次の3つのメソッドしかありませんでした。
jshell> MemoryHandles.
class varHandle( withOffset( withStride(
Java 16ではMemoryAccessというユーティリティが用意されて、VarHandleを使わずにシンプルにかけるようになっています。
import jdk.incubator.foreign.*;
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
for (int i = 0 ; i < 25 ; i++) {
MemoryAccess.setIntAtOffset(i * 4, i);
}
}
JEP 389: Foreign Linker API (Incubator)
ネイティブライブラリの呼び出しを行う
外部メモリのアクセスにはForeign Memory Access APIを使う
JNIの代替
JEP 396: Strongly Encapsulate JDK Internals by Default
sun.misc.Unsafeなど以外の内部APIの隠蔽をデフォルトにします。
内部APIから標準APIへの移行がやりやすいようにします。
Java 9で内部APIを隠蔽する仕組みが導入されていましたが、デフォルトでは無効になっていました。それが今回有効化されました。
次のオプションを利用します。15まではpermitがデフォルトでしたが、denyがデフォルトになりました。
オプション | APIの利用 | 警告 | スタックトレース | デフォルト |
---|---|---|---|---|
--illegal-access=permit | 可 | 9から15まで | ||
--illegal-access=warn | 可 | 出力 | ||
--illegal-access=debug | 可 | 出力 | 出力 | |
--illegal-access=deny | 不可 | 16でデフォルト |
制限される内部APIの一覧はこちらです。
https://cr.openjdk.java.net/~mr/jigsaw/jdk8-packages-denied-by-default
jshell> A.class.getClassLoader().loadClass("com.sun.beans.finder.ClassFinder").getMethod("findClass", String.class).invoke("test")
| 例外java.lang.IllegalAccessException: class REPL.$JShell$15 cannot access class com.sun.beans.finder.ClassFinder (in module java.desktop) because
module java.desktop does not export com.sun.beans.finder to unnamed module @77556fd
| at Reflection.newIllegalAccessException (Reflection.java:385)
| at AccessibleObject.checkAccess (AccessibleObject.java:687)
| at Method.invoke (Method.java:559)
| at (#5:1)
[JDK-8180352] Add Stream.toList() method - Java Bug System
JavaDoc: Stream.toList
collect(Collectors.toList)がめんどくさすぎたので、toListが導入されました。
変更不能なListになってますね。
C:\Users\naoki>java\jdk\jdk-16-ea31\bin\jshell
| JShellへようこそ -- バージョン16-ea
| 概要については、次を入力してください: /help intro
jshell> Stream.of("a","b").map(String::toUpperCase).toList()
$1 ==> [A, B]
jshell> $1.getClass()
$2 ==> class java.util.ImmutableCollections$ListN
いままでStreamとCollectionフレームワークを分離したいという意向で、Collectors経由でのtoListを行っていたのですが、なにをきっかけに気がかわったのか、突然とりこまれました。
BTSは2017年からありますね。
http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-November/070782.html
https://github.com/openjdk/jdk/pull/1026
Stream.mapMulti
値を次々に生成するような処理をStream中にはさみたい場合に使います。
Stream.of("a", "b")
.mapMulti((s, sink) -> sink.accept(s.toUpperCase()))
.toList()
ここで、sにはStreamを流れる値で、sinkに対してacceptしたものが次に渡されていきます。
http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-July/067560.html
[JDK-8254876] (fs) NullPointerException not thrown when first argument to Path.of or Paths.get is null - Java Bug System
Path.ofに複数の引数わたしたとき、最初の引数がnullだと変なオブジェクトが生成されていました。このPathを使うところでぬるぽが発生していたはず。
C:\Users\naoki>jshell
| JShellへようこそ -- バージョン15
| 概要については、次を入力してください: /help intro
jshell> Path.of(null, "r")
$1 ==> null\r
Java 16では最初の引数がnullだったらぬるぽ出るようになっています。
C:\Users\naoki>java\jdk\jdk-16\bin\jshell
| JShellへようこそ -- バージョン16
| 概要については、次を入力してください: /help intro
jshell> Path.of(null, "r")
| 例外java.lang.NullPointerException
| at Objects.requireNonNull (Objects.java:208)
| at WindowsFileSystem.getPath (WindowsFileSystem.java:214)
| at Path.of (Path.java:147)
| at (#1:1)
JVM
JEP 376: ZGC: Concurrent Thread-Stack Processing
JEP 387: Elastic Metaspace
[JDK-8255616] Removal of experimental features AOT and Graal JIT - Java Bug System
[JDK-8202343] Disable TLS 1.0 and 1.1 - Java Bug System
JEP 376: ZGC: Concurrent Thread-Stack Processing
ZGCではPause Mark StartフェーズとPause Relocate Startフェーズでスレッドスタックのスキャンを行っていた。このふたつのフェーズはstop-the-worldを行う。
つまりアプリケーションのルートセットサイズがこのふたつのフェーズの停止時間を決めていた。
アプリケーションスレッドと並列にすることでSTWではスレッドスタックがスキャンされなくなる。つまりSTWがアプリケーションのルートセットのサイズへの依存が少なくなる。
16TBでも1ms以下の停止時間でZGCを使える
スレッドスタックの処理をsafepointからconcurrentフェーズに移動する。
スタック処理を遅延させて並列でインクリメンタルにする
他のスレッドごとのルート処理をZGC safepointから削除する
他のHotSpotのサブシステムがスタックを遅延実行できるような仕組を提供する
典型的なマシンで1ミリ秒以下でsafepointの処理が行えるようにする
大量にスレッドがあるとスレッドごとのルート処理が問題になる
JEP 387: Elastic Metaspace
クラスのデータなど、JVMが利用するデータはMetaspaceに保存されますが、このようなデータが使用済みになったときに返却するようにして、効率的にMetaspaceが利用できるようにします。
[JDK-8255616] Removal of experimental features AOT and Graal JIT - Java Bug System
Experimentalとして入っていたAOTコンパイラやGraal JITコンパイラがOracleのOpenJDKビルドからは削除されているようです。
つまり、java.netでダウンロードしたバイナリには含まれなくなっています。他のディストリビューションがどうなっているか確認していないけど、恐らく無効にするんじゃなかろうか。GraalVMはデフォルトでGraal JITを使うので残りますね。
ソースコードには残っているようなので、自分でビルドすれば試すことができます。
[JDK-8202343] Disable TLS 1.0 and 1.1 - Java Bug System
TLS 1.0と1.1が無効化されています。
そのため、古いバージョンのMySQLへの接続などがエラーになることがあるようです。
ツール
JEP 392: Packaging Tool
Incubatorとして入っていたパッケージングツールが標準化する。
Javaアプリケーションのインストーラを作るツールが入りました。
関連APIも追加されていますが、Incubatorモジュールになっています。
Windowsではlight.exeとcandle.exeが必要なのでhttps://wixtoolset.org からダウンロードしてPATHに追加する必要があります。
試しに次のアプリケーションのインストーラを作ってみます。
import javax.swing.*;
public class App {
public static void main(String[] args) {
var f = new JFrame("My App");
var t = new JTextArea();
f.add(t);
var b = new JButton("Hello");
b.addActionListener(al -> t.append("Hello!\n"));
f.add("North", b);
f.setSize(500, 400);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}
}
アプリケーションはJarファイルになっている必要があります。
$ javac App.java
$ mkdir target
$ jar cf target/app.jar App.class
$ jpackage --name myapp --input target --main-jar app.jar --main-class App
そうするとWindowsの場合はmyapp.exeという名前で45MBのインストーラが作成されます。
モジュラーJARの場合は自動的に必要なモジュールのみのJavaランタイムがインストールされるようになりますが、ここではモジュール対応しないJarファイルを作ったので、jlinkを使って最低限のJavaランタイムを作ると小さなインストーラが作れるようになります。
$ jdeps --list-deps App.class
java.base
java.desktop
$ jlink --add-modules java.base,java.desktop --output jre
$ jpackage --name myapp --input target --main-jar app.jar --main-class App --runtime-image jre
そうすると27MBまでインストーラのサイズが削減されました。
インストール後のアプリケーションサイズも124MBから74MBに減ります。
Windowsの場合は--win-menu
をつけてWindowsメニューにアプリが追加されるようにするほうがいいでしょう。
OpenJDK
JEP 347: Enable C++14 Language Features
JEP 357: Migrate from Mercurial to Git
JEP 369: Migrate to GitHub
JEP 386: Alpine Linux Port
JEP 388: Windows/AArch64 Port
JEP 347: Enable C++14 Language Features
C++14の機能をJDK開発に利用できるようにします。
JEP 357: Migrate from Mercurial to Git
Mercurialで管理されていたOpenJDKのソースコードをGitでの管理に変更します。
プロジェクトSkaraとして進んでいました。
OpenJDK: Skara
JEP 369: Migrate to GitHub
こちらもプロジェクトSkaraとして進んでいましたが、開発管理をGitHubベースにするものです。
JEP 386: Alpine Linux Port
Alpine Linuxを始めとして、Cライブラリにmuslを使うようなLinuxシステムに対応します。
各ディストリビューションでもAlpine Linux対応バイナリを提供するようになっています。
jlinkを使って実行環境をつくりなおすと、Spring Bootを含めて77MBのイメージを作ることができました。
ところで、Alpineでビルドするとsrc/hotspot/os/linux/os_linux.cpp
でエラーが出るので、とりあえずこんな感じで対処しておく必要がありました。
672c672
< alloca(((pid ^ counter++) & 7) * 128);
---
> int* dummy = (int*)alloca(((pid ^ counter++) & 7) * 128);
って言ってたらパッチあててもらったのでJava 17やJava 16.0.1では対処されるんじゃないかと思います
JEP 388: Windows/AArch64 Port
Arm Windows対応。
Arm Mac対応のJEPもあるけど、間に合わなかった。Java 17になるようです。
JEP 391: macOS/AArch64 Port