OpenJDKとAppCDS

Oracle JDKのclosed repositoryにあった機能がOpenJDKにマージしていくと今年のJavaOne 2017で発表があった。この一環でOpenJDKにAppCDS (Application Class Data Sharing) が11/27(日本時間だと11/28)に入ってきた。今日はこの機能を動かして試していきたい。どうせだからクラッシュの一つでもして欲しい。

とは言ってもOracle JDKにはJDK8u40からすでにあるので、知ってる人は既に知っている内容である。

前提

  • jdk/hsのchangeset48049以降で実行する。ビルドは以下の通りに実行できる。コマンドではリビジョン指定は特にしていないが12/01の時点でAppCDSは利用可能。
$ hg clone http://hg.openjdk.java.net/jdk/hs
$ cd hs
$ bash configure --with-extra-cflags="-Wno-error=deprecated-declarations -Wno-error=nonnull -Wno-error=maybe-uninitialized" --disable-warnings-as-errors
$ make images
$ ${PWD}/build/linux-x86_64-normal-server-release/images/jdk/bin/java -version
openjdk version "10-internal"
OpenJDK Runtime Environment (build 10-internal+0-adhoc.fedora.hs)
OpenJDK 64-Bit Server VM (build 10-internal+0-adhoc.fedora.hs, mixed mode)

CDS (Class Data Sharing) とはなにか

(システム)JARファイルから利用するクラスをJVM内部表現(private internal representation)にロードしてダンプした「共有アーカイブ(shared archives)」と呼ばれるクラスの読取専用メタデータを含むファイルを作成することができる。JVMを起動する際に1からクラスをロードするよりも、このファイル(共有アーカイブ)をメモリマッピングして復元する方が早いので起動時間が短縮されると言う仕組み。

この「共有アーカイブ」は名前の通り複数JVM間で共有できるため、複数JVMプロセスで共有することでフットプリント(dynamic memory footprint)も抑えられる。JREのインストール手段次第だがデフォルトで共有アーカイブが導入される。自分で手動で作ることも可能。

CDSはシステムJARファイルが対象だったが、AppCDSはそれに加えてユーザのアプリケーションコードまで対象となったものである。
ちゃんと言うと、CDSはブートストラップクラスローダの対象クラスのみだったが、AppCDSはそれに加えてビルトインシステムクラスローダー(アプリケーションクラスローダーとも言う)、プラットフォームクラスローダーとカスタムクラスローダーの対象クラスをアーカイブできる。

AppCDS を試す

本来はクラスが大量なアプリケーションで実行するのが効果が見えて良いのだが、手軽に準備できるものががないのでSpring Initializrのデフォルト値で作成したデモアプリdemo-0.0.1-SNAPSHOT.jarの起動時間を改善してみよう。

デフォルトは以下のような感じ。

$ java -cp target/demo-0.0.1-SNAPSHOT.jar org.springframework.boot.loader.JarLauncher

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.8.RELEASE)
:
2017-12-01 06:10:17.856  INFO 27529 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.103 seconds (JVM running for 1.841)
:

1.103 secondsがベースライン。

アーカイブするクラスのリストを作る

共有アーカイブを作成するために、先ずは以下のコマンドで利用しているクラスを抽出する。

$ java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=demo.list -cp target/demo-0.0.1-SNAPSHOT.jar org.springframework.boot.loader.JarLauncher
:
2017-12-01 06:09:04.430  INFO 27451 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 320.159 seconds (JVM running for 454.536)
:

アプリケーションを動作させつつ抽出された利用クラスのリストdemo.listをゲット。抽出しつつなのでアホみたいに起動時間が掛かっている。中身は以下のようにクラスの一覧になっている。

$ cat demo.list
java/lang/Object
java/lang/String
:
org/springframework/boot/loader/JarLauncher
org/springframework/boot/loader/ExecutableArchiveLauncher
:

勿論だが、一応手で書き加えることも可能。

共有アーカイブを作成する

クラスの一覧ができたので一度ロードしてダンプさせ、共有アーカイブを作成する。

$ /home/fedora/workspace/hs/build/linux-x86_64-normal-server-release/images/jdk/bin/java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=demo.list -XX:SharedArchiveFile=demo.jsa -XX:+IgnoreUnverifiableClassesDuringDump -cp target/demo-0.0.1-SNAPSHOT.jar
narrow_klass_base = 0x0000000800000000, narrow_klass_shift = 3
Allocated temporary class space: 1073741824 bytes at 0x00000008c0000000
Allocated shared space: 3221225472 bytes at 0x0000000800000000
Loading classes to share ...
Loading classes to share: done.
Rewriting and linking classes ...
Rewriting and linking classes: done
Number of classes 1866
    instance classes   =  1789
    obj array classes  =    69
    type array classes =     8
Updating ConstMethods ... done.
Removing unshareable information ... done.
Scanning all metaspace objects ...
Allocating RW objects ...
Allocating RO objects ...
Relocating embedded pointers ...
Relocating external roots ...
Dumping symbol table ...
Dumping String objects to closed archive heap region ...
Dumping objects to open archive heap region ...
Relocating SystemDictionary::_well_known_klasses[] ...
Removing java_mirror ... done.
mc  space:      8800 [  0.0% of total] out of     12288 bytes [ 71.6% used] at 0x0000000800000000
rw  space:   5381944 [ 22.2% of total] out of   5382144 bytes [100.0% used] at 0x0000000800003000
ro  space:   9586008 [ 39.5% of total] out of   9588736 bytes [100.0% used] at 0x0000000800525000
md  space:      6160 [  0.0% of total] out of      8192 bytes [ 75.2% used] at 0x0000000800e4a000
od  space:   8581008 [ 35.3% of total] out of   8581120 bytes [100.0% used] at 0x0000000800e4c000
st0 space:    634880 [  2.6% of total] out of    634880 bytes [100.0% used] at 0x00000007bff00000
oa0 space:     81920 [  0.3% of total] out of     81920 bytes [100.0% used] at 0x00000007bfe00000
total    :  24280720 [100.0% of total] out of  24289280 bytes [100.0% used]

共有アーカイブdemo.jsaをゲット。

早くなったか確認する

作った共有アーカイブをロードさせることで早くなったか確認してみよう。

$ /home/fedora/workspace/hs/build/linux-x86_64-normal-server-release/images/jdk/bin/java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=demo.jsa -cp target/demo-0.0.1-SNAPSHOT.jar org.springframework.boot.loader.JarLauncher
:
2017-12-01 06:20:39.487  INFO 28908 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.992 seconds (JVM running for 1.414)
:

0.992 secondsとなり約1割早くなりました、良い感じですね。絶対数にするとアレですがAppCDSはその特性上クラスが多いほど効くので、依存性が多いアプリケーションではより有効となる可能性がある。

実際にどうなってるか確認する

クラスローディングが共有アーカイブまたはライブラリ(クラスファイル)から行われているかで確認することができる。クラスローディングは-Xlog:class+load=infoでロギングすることが可能。

AppCDS有効

コアライブラリもメインクラスもshared objects fileからロードしていることが解る。

$ /home/fedora/workspace/hs/build/linux-x86_64-normal-server-release/images/jdk/bin/java -Xlog:class+load=info -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=demo.jsa -cp target/demo-0.0.1-SNAPSHOT.jar org.springframework.boot.loader.JarLauncher
[0.003s][info][class,load] opened: /home/fedora/workspace/hs/build/linux-x86_64-normal-server-release/images/jdk/lib/modules
[0.055s][info][class,load] java.lang.Object source: shared objects file
[0.055s][info][class,load] java.io.Serializable source: shared objects file
[0.055s][info][class,load] java.lang.Comparable source: shared objects file
:
[0.098s][info][class,load] org.springframework.boot.loader.Launcher source: shared objects file
[0.098s][info][class,load] org.springframework.boot.loader.ExecutableArchiveLauncher source: shared objects file
[0.098s][info][class,load] org.springframework.boot.loader.JarLauncher source: shared objects file
:

AppCDS 無効

有効時とは変わってライブラリファイルから読み込んでいることが解る。

$ /home/fedora/workspace/hs/build/linux-x86_64-normal-server-release/images/jdk/bin/java -Xlog:class+load=info -Xshare:off -cp target/demo-0.0.1-SNAPSHOT.jar org.springframework.boot.loader.JarLauncher
[0.003s][info][class,load] opened: /home/fedora/workspace/hs/build/linux-x86_64-normal-server-release/images/jdk/lib/modules
[0.013s][info][class,load] java.lang.Object source: jrt:/java.base
[0.014s][info][class,load] java.io.Serializable source: jrt:/java.base
[0.014s][info][class,load] java.lang.Comparable source: jrt:/java.base
:
[0.131s][info][class,load] org.springframework.boot.loader.Launcher source: file:/home/fedora/workspace/startup/demo/target/demo-0.0.1-SNAPSHOT.jar
[0.131s][info][class,load] org.springframework.boot.loader.ExecutableArchiveLauncher source: file:/home/fedora/workspace/startup/demo/target/demo-0.0.1-SNAPSHOT.jar
[0.131s][info][class,load] org.springframework.boot.loader.JarLauncher source: file:/home/fedora/workspace/startup/demo/target/demo-0.0.1-SNAPSHOT.jar
:

まとめ

他にも細々としたオプションが用意されているが、ざっと見たところアナウンス通りOracle JDKで利用できる機能は来ているようです。フレームワークを利用している単純なデモアプリだと約一割の削減でしたが、クラス数がより多いアプリだともっと効果が見込めるかもしれません。開発時に使ってみるのはいかがでしょうか。

変にAOTと組み合わせたりしたらクラッシュしたりしないかなこれ。

【番外編】もっと早くする

/home/fedora/workspace/hs/build/linux-x86_64-normal-server-release/images/jdk/bin/java -Xlog:all=off -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xverify:none -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=demo.jsa -cp target/demo-0.0.1-SNAPSHOT.jar org.springframework.boot.loader.JarLauncher
2017-12-01 07:14:41.301  INFO 29775 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.811 seconds (JVM running for 1.178)

0.811 secondsでした!jlinkで不要なプラットフォームモジュールをばっさり削った極小カスタムランタイムもやってみたかったところですが、時間がなかったので今日はここまで!

JVMの起動時間の短縮にはこういったランタイムに対する工夫の他に、JVMを立ち上げ続けて実行させる方法もある。アプローチは色々あるが、実装例としてはnailgunや、SO_REUSEPORTを利用して無停止デプロイを実装したfalchionなどを見てみると面白いかもしれません。