mike-neckのブログ

Java or Groovy or Swift or Golang

Java Day Tokyo 2018 のセッションメモ

Java Day Tokyo 2018 に行ってきたので、そのメモ。


キーノート

目黒駅で東京駅南口行きバスのりばを探すのに時間がかかってしまったため、20分遅刻してしまった。

僕が参加したときには Java の新しいリリースモデルの説明。

OpenJDK は 半年間しかサポートしないので、LTSが欲しい場合は Oracle または他のベンダーに頼る感じと思われる。


Updates from the JCP Program

トラブルで最初の10分くらい何を言っているかわからなかった(通訳レシーバーが故障してた)。

JCP/JSR がどういったもので、 Java に貢献するにはどうしていくかという話だった


Project Vallhala

Value Type というプリミティブをフィールドとするデータ型を定義するようにして、その配列が効率的になるようにメモリの持ち方を工夫するというプロジェクト。

これをさらに進めると、ジェネリクスにプリミティブ型を指定できるようになり、 Stream<Integer> とか IntStream ではなく、 Stream<int> のように扱えるようになる。ただ、型の変位については問題があるようで、 int が継承しているクラスは一体何なのかなどの整理が必要である様子。

また、 Unsafe を置き換えるもので VarHandles があるが、それらの概要を示した上で、 JJUG CCC でそのセッションをやるとのことだった。


Curing you Domain Model Anemia with Effective & Clean Tips from the Real World

どういったモデルを書いていけば、集約度の高いクラスが書けるか的な話。DDD好きにはたまらないやつ。

JPAのエンティティを例にサンプルを書いていってたが、僕が普段意識して書いているコードに近いものになってた。

  • コンストラクターではなく、スタティックファクトリーメソッドを提供する
  • エンティティクラスにはデフォルトコンストラクターだけではなく、フィールドに値を突っ込めるコンストラクターも提供する(そのコンストラクターはファクトリーメソッドからのみアクセスする)
  • エンティティクラスの中で特定の値域をもつようなフィールドは、プリミティブな値をそのまま持つのではなく、 Embeddedable クラスを作ってそちらで検証する
  • コレクションフィールドのゲッターは不変(Collections.unmodifiableList() など使って)にしたコレクションを返す。値の追加はこのクラスの中でのみおこなう

Java SE10/11 移行ガイド

すごい混んでたセッション。引っ込み思案なため、席がとれなくて、床に座ってセッションを聞いてた。

雑なまとめ

  • @Deprecated になっている API は使うな。@Deprecated(forRemoval = true) となっているものはまじでなくなるので使うな
  • --add-exports + ALL-UNNAMED/--add-opens などコンパイルオプションが多い
    • モジュール周りはドキュメント自分で読まないと詳しくなれなそう
  • 一つのパッケージを複数のモジュールに組み込めないので、 現在 Java SE にある JAXB/JAX-WSは取り除かれる。使いたい場合は Java EE から提供されているものを使うこと
    • ただ、 javax.activationmaven central にもないので、自分でパッケージングが必要になりそう…
  • ライブラリーを作っている人は META-INF.MFAutomatic-Module をつけてくれ。でないと、jar ファイルの名前がモジュール(Unnamed-Module)になるのでバージョンが変わるたびにビルドが壊れる

たぶん、このメモ読んでも移行できないので、資料を見たほうがよいかもしれない


Project Loom

軽量プロセス Fiber についてのセッション。今回もっとも面白かったセッション。

  • FiberJava の runtime やユーザーコードに支配されるスレッドで、 Thread よりもコストが低く、 OS のスレッドの 1000倍の個数を扱える
  • サーバーなどのリソースの活用の仕方をモニターすると、 セッションの生存期間が長くて数も増えるが、殆どの時間が I/O 待ちでリソースが有効活用されていない
  • 簡単だが性能がよくない同期モデルのプログラミングにするか、複雑になるが高性能の非同期プログラミングモデルにするかの選択しかなかったが、 Fiber を用いると高性能で簡単なモデルのプログラミングが出来るようになる
  • Fiber をこれまでの Thread の上に作るか、 Thread の parent インターフェースとして Strand を作り、 Fiber インターフェースを Strand を継承したものにするか、どちらかがAPI の候補となっている
  • Fiber を使うと、JAX-RS のコードのスループットが5〜10倍になる
  • もしすでに非同期で書いていたら…それを同期に戻すような修正が必要になるかもしれない
  • Fiber を使うと、golang のチャネルや Python のジェネレーターのようなものが作れるようになる
  • Fiberシリアライズして別のJVMに送ってそちらで計算させることができる(coherence のようなデータがある所で処理させるような用途が可能/超長時間のトランザクションなども実現できる)
  • 課題: Fiber に対応させるために既存の java.netjava.nio 周りを書き直さないといけない

あまり興奮を伝えられていないが、本当に Fiber 最強やんけという感想しかなかった

CompletableFuture の thenApplyAsync メソッドは何度も呼び出せるのか試してみた

最近、自分専用のライブラリーを書いていて、 CompletableFuture#thenApply および CompletableFuture#thenApplyAsync などを複数回呼び出せるのかがわかっていなかったし、 Javadoc にも書いていないようなので実験してみることにしました。


書いたのは次のようなコードです。

@Test
void handlers2() throws InterruptedException {
  final CountDownLatch latch = new CountDownLatch(1);
  final CompletableFuture<String> future =
      CompletableFuture.supplyAsync(
          () -> {
            sleep();
            // ランダムで結果を返す
            // 成功の場合は foo を返す
            // 失敗の場合はRuntimeExceptionが発生
            return Result.random().apply("foo");
          },
          executor);

  // CompletableFuture#thenApplyAsync を2回呼び出す
  final CompletableFuture<String> f1 =
      future.thenApplyAsync(str -> String.format("result -> %s", str));
  final CompletableFuture<String> f2 =
      future.thenApplyAsync(
          str -> {
            throw new RuntimeException(str);
          });

  // 結果を文字列で表示するためのハンドラー
  final CompletableFuture<String> hf1 =
      f1.whenCompleteAsync(
          (str, th) ->
              handle(str, th)
                  .onSuccess(s -> System.out.println(String.format("handler 1 -> %s", s)))
                  .onError(
                      e ->
                          System.out.println(
                              String.format(
                                  "handler 1 error -> %s", e.getClass().getSimpleName()))),
          executor);
  final CompletableFuture<String> hf2 =
      f2.whenCompleteAsync(
          (str, th) ->
              handle(str, th)
                  .onSuccess(s -> System.out.println(String.format("handler 2 -> %s", s)))
                  .onError(
                      e ->
                          System.out.println(
                              String.format(
                                  "handler 2 error -> %s", e.getClass().getSimpleName()))),
          executor);
  CompletableFuture.allOf(hf1, hf2).whenComplete((s, t) -> latch.countDown());
  latch.await();
}

結果は次のようになりました

handler 1 -> result -> foo
handler 2 error -> CompletionException

というわけで、同じ CompletableFutureインスタンスから複数の CompletableFuture を派生させることが可能とわかった


すると次に気になるのは、 CompletableFuture の結果が返ってきてから、 thenApplyAsync を呼び出せるのかという問題

先ほどのメソッドを次のように書き足してみた

@Test
void handlers2() throws InterruptedException {
  // CountDownLatch を追加
  final CountDownLatch finalLatch = new CountDownLatch(1);

  // ... 先ほどのメソッド

  latch.await(); // 先ほどのメソッドの最後の行

  final CompletableFuture<String> f3 =
      future.thenApplyAsync(str -> String.format("after completed: %s", str));
  final CompletableFuture<String> hf3 =
      f3.whenCompleteAsync(
          (str, th) ->
              handle(str, th)
                  .onSuccess(s -> System.out.println(String.format("handler 3 -> %s", s)))
                  .onError(
                      e ->
                          System.out.println(
                              String.format(
                                  "handler 3 error -> %s", e.getClass().getSimpleName()))),
          executor);

  hf3.whenCompleteAsync((s,t) -> finalLatch.countDown());
  finalLatch.await();
}

実行結果はこちら

handler 2 error -> CompletionException
handler 1 -> result -> foo - foo
handler 3 -> after completed: foo - foo

で、 complete した状態の CompletableFuture に対して、関数を適用することも可能だということがわかった。

Component の インデックススキャンでどれくらい速くなるか計測してみた #jsug

JSUG のLT会で 『Spring Boot アプリケーションの起動をほんの少し気持ちだけ速くしてみた』というタイトルでLTしてきたところ、 Spring 5 から @Component のインデックススキャンがサポートされるようになったことを教えてもらったので、試してみました。


追加するライブラリー

以下のとおり、アノテーションプロセッサーにライブラリーを追加します。

  annotationProcessor('org.springframework:spring-context-indexer')

あとは普通にビルドするだけです。

なおビルドすると、 /META-INF の下に spring.components というファイルができます

中身はこんな感じです(一部抜粋)

#
#Wed Apr 18 23:17:29 JST 2018
com.example.jpa.repository.baz.BazRepository=org.springframework.stereotype.Component,org.springframework.data.repository.Repository
com.example.jpa.entity.def.DefEntity=javax.persistence.Entity,javax.persistence.Table
com.example.jpa.repository.waldo.WaldoRepository=org.springframework.stereotype.Component,org.springframework.data.repository.Repository
com.example.App=org.springframework.stereotype.Component
com.example.jpa.entity.garply.GarplyEntity=javax.persistence.Entity,javax.persistence.Table

起動時間の比較

では、早速比較してみます。

  • slow というプロジェクトが何もしないままのプロジェクト
  • indexed というプロジェクトが slow プロジェクトをコピーしてインデックススキャンを有効にしたプロジェクト

次のスクリプトを実行します

#!/usr/bin/env bash

set -e

./gradlew slow:clean slow:bootJar indexed:clean indexed:bootJar

echo slow
java -jar slow/build/libs/slow-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"
echo indexed
java -jar indexed/build/libs/indexed-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"

echo slow
java -jar slow/build/libs/slow-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"
echo indexed
java -jar indexed/build/libs/indexed-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"

echo slow
java -jar slow/build/libs/slow-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"
echo indexed
java -jar indexed/build/libs/indexed-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"

実行結果は次のようになりました

BUILD SUCCESSFUL in 4s
10 actionable tasks: 10 executed
slow
 in 6.723 seconds (JVM running for 7.572)
indexed
 in 6.012 seconds (JVM running for 6.875)
slow
 in 6.059 seconds (JVM running for 6.889)
indexed
 in 6.129 seconds (JVM running for 6.996)
slow
 in 6.057 seconds (JVM running for 6.874)
indexed
 in 6.025 seconds (JVM running for 6.924)
  • 何もしない場合の起動時間 : 6.280 sec
  • インデックスつけた場合の起動時間 : 6.055 sec

という感じで、少し速くなりました。が、2回目などは遅くなっている場合もあり、もう少し大きめなプロジェクトで試してみたい気もします

Class Data Sharing を試してみる

Java 10 の Class Data Sharing で Spring Boot の起動を速くしてみます。


Class Data Sharing は異なるJVM上で同一のクラスの情報を共有する仕組みです。 Java 8 の時点ですでに組み込まれていましたが、コマーシャルな機能であったため、 使っている人は少ないと思います。 Java 10 からはこの機能が OpenJDK でも利用できるようになったため、早速試してみたいと思います。


Spring Boot アプリケーション

実行対象とするアプリケーションを作るために次のコマンドでプロジェクトを作ります

gradle init --type=java-library
curl https://start.spring.io/build.gradle \
  -d dependencies=webflux,actuator,data-jpa,thymeleaf,\
validation,data-redis-reactive,flyway,retry,\
statemachine,mail,h2 > build.gradle

次に次のようなメインクラスを作ります。

@SpringBootApplication
public class App {
  public static void main(String... args) {
    SpringApplication.run(App.class, args);
  }

  @Bean
  CommandLineRunner commandLineRunner() {
    return args -> {};
  }

  private final Foo foo;

  App(final Foo foo) {
    this.foo = foo;
  }

  public static class Foo {}
}

JVMの起動時間だけが今は必要なので、SpringのBean読み込みが始まったらすぐに落ちるメインクラスになっています。


Spring Boot アプリケーションの起動

Spring Boot アプリケーションを起動したいのですが、 gradle の bootRun タスクに jvm パラメーターを指定する方法でやると、 Class Data のアーカイブディレクトリーがうまく認識されなかったので、 bootJar で作成した jar ファイルを JavaExec タスクで起動します。

task createCdsDirectory {
  outputs.dir(cdsDirectory)
  doLast {
    if (!cdsDirectory.exists()) {
      cdsDirectory.mkdirs()
    }
  }
}

task runBootJar(type: JavaExec) {
  main = 'org.springframework.boot.loader.JarLauncher'
  dependsOn bootJar
  classpath bootJar
  def create = false
  if (project.hasProperty('listApp')) {
    jvmArgs = ['-XX:+UnlockCommercialFeatures', '-Xshare:off', "-XX:DumpLoadedClassList=${classList}", '-XX:+UseAppCDS']
    create = true
  } else if (project.hasProperty('dumpApp')) {
    jvmArgs = ['-XX:+UnlockCommercialFeatures', '-Xshare:dump', "-XX:SharedClassListFile=${classList}", '-XX:+UseAppCDS', "-XX:SharedArchiveFile=${archiveFile}"]
    create = true
  } else if (project.hasProperty('runApp')) {
    jvmArgs = ['-XX:+UnlockCommercialFeatures', '-Xshare:on', '-XX:+UseAppCDS', "-XX:SharedArchiveFile=${archiveFile}"]
  }
  if (create) {
    dependsOn createCdsDirectory, showStartTime
  } else {
    dependsOn showStartTime
  }
  finalizedBy showEndTime
}

Class Data Sharing を使う場合、次の3つのフェーズがあり、それぞれで別のパラメーターを必要とします。

アーカイブ対象リストを取得
  • -XX:+UnlockCommercialFeatures
  • -XX:+UseAppCDS
  • -Xshare:off
  • -XX:DumpLoadedClassList=${classList}
アーカイブファイルを取得
  • -XX:+UnlockCommercialFeatures
  • -XX:+UseAppCDS
  • -Xshare:dump
  • -XX:SharedClassListFile=${classList}
  • -XX:SharedArchiveFile=${archiveFile}
アーカイブファイルを読みこんでアプリケーションを起動する
  • -XX:+UnlockCommercialFeatures
  • -XX:+UseAppCDS
  • -Xshare:on
  • -XX:SharedArchiveFile=${archiveFile}

ここでは、 gradle のプロパティ値の有無で起動オプションを変更する形で起動します。

また、実行時間を計測する用途として次の二つのタスクを作ります。

task showStartTime(group: 'show-time') {
  doLast {
    def now = LocalDateTime.now()
    startTime << now
    logger.lifecycle('start {}', now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
  }
}

task showEndTime(group: 'show-time') {
  doLast {
    def now = LocalDateTime.now()
    if (!startTime.empty) {
      LocalDateTime start = startTime[0]
      logger.lifecycle('start {}', start.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
      logger.lifecycle('end {}', now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
      def duration = Duration.between(start, now)
      logger.lifecycle('time: {} ms', duration.toMillis())
    }
  }
}

Class Data Sharing を使わない場合

Class Data Sharing を使わない場合の起動時間を確認します。

$ ./gradlew clean runBootJar 

> Task :showStartTime 
start 2018-04-01T19:41:48.800745

> Task :runBootJar 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.0.RELEASE)

2018-04-01 19:41:55.441  INFO 8454 --- [           main] com.example.App                          : Starting App on mac.local with PID 8454 (/path/to/project/build/libs/spring-app-cds-sample-0.0.1-SNAPSHOT.jar started by mike in /path/to/project)
2018-04-01 19:41:55.447  INFO 8454 --- [           main] com.example.App                          : No active profile set, falling back to default profiles: default

... 中略

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.App required a bean of type 'com.example.App$Foo' that could not be found.


Action:

Consider defining a bean of type 'com.example.App$Foo' in your configuration.


> Task :showEndTime 
start 2018-04-01T19:41:48.800745
end 2018-04-01T19:41:59.122787
time: 10322 ms

... 以下省略

JVM起動開始 → クラスロード完了 → Spring Boot アプリケーションの起動 → Spring Boot アプリケーションの終了 の一連の流れで 10322 ms ほどかかりました。


Class Data Sharing を使う場合

クラスリストの取得

最初にアーカイブ対象のクラスをリスト化します。

$ ./gradlew runBootJar -PlistApp

> Task :showStartTime 
start 2018-04-01T19:32:34.882318

> Task :runBootJar 
skip writing class com/sun/proxy/$Proxy0 from source __JVM_DefineClass__ to classlist file
skip writing class com/sun/proxy/$Proxy1 from source __JVM_DefineClass__ to classlist file
skip writing class com/sun/proxy/$Proxy2 from source __JVM_DefineClass__ to classlist file
skip writing class com/sun/proxy/$Proxy3 from source __JVM_DefineClass__ to classlist file
skip writing class com/sun/proxy/$Proxy9 from source __JVM_DefineClass__ to classlist file
skip writing class com/sun/proxy/jdk/proxy1/$Proxy11 from source __JVM_DefineClass__ to classlist file
skip writing class java/lang/invoke/BoundMethodHandle$Species_LII from source __JVM_DefineClass__ to classlist file
skip writing class java/lang/invoke/BoundMethodHandle$Species_LIIL from source __JVM_DefineClass__ to classlist file
skip writing class java/lang/invoke/BoundMethodHandle$Species_LIILL from source __JVM_DefineClass__ to classlist file
skip writing class java/lang/invoke/BoundMethodHandle$Species_LIILLL from source __JVM_DefineClass__ to classlist file

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.0.RELEASE)

2018-04-01 19:32:41.717  INFO 8413 --- [           main] com.example.App                          : Starting App on mac.local with PID 8413 (/path/to/paroject/build/libs/spring-app-cds-sample-0.0.1-SNAPSHOT.jar started by mike in /path/to/project)
2018-04-01 19:32:41.725  INFO 8413 --- [           main] com.example.App                          : No active profile set, falling back to default profiles: default
skip writing class com/sun/proxy/$Proxy13 from source __JVM_DefineClass__ to classlist file
skip writing class com/sun/proxy/$Proxy24 from source __JVM_DefineClass__ to classlist file
2018-04-01 19:32:41.853  INFO 8413 --- [           main] onfigReactiveWebServerApplicationContext : Refreshing org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@1139b2f3: startup date [Sun Apr 01 19:32:41 JST 2018]; root of context hierarchy
skip writing class java/lang/invoke/BoundMethodHandle$Species_LLLII from source __JVM_DefineClass__ to classlist file
skip writing class java/lang/invoke/BoundMethodHandle$Species_LLLIIL from source __JVM_DefineClass__ to classlist file
skip writing class java/lang/invoke/BoundMethodHandle$Species_LLLIILL from source __JVM_DefineClass__ to classlist file
skip writing class java/lang/invoke/BoundMethodHandle$Species_LLLIILLL from source __JVM_DefineClass__ to classlist file
skip writing class java/lang/invoke/BoundMethodHandle$Species_LLLIILLLL from source __JVM_DefineClass__ to classlist file

... 中略

> Task :showEndTime 
start 2018-04-01T19:32:34.882318
end 2018-04-01T19:32:45.295433
time: 10413 ms

... 以下省略

これによって、次のようなテキストファイルが作成されます。(このサンプルでは build/cds/list.txt が作成される)

$ head -10 build/cds/list.txt 
java/lang/Object
java/lang/String
java/io/Serializable
java/lang/Comparable
java/lang/CharSequence
java/lang/Class
java/lang/reflect/GenericDeclaration
java/lang/reflect/AnnotatedElement
java/lang/reflect/Type
java/lang/Cloneable
$ grep spring build/cds/list.txt  | head -10
org/springframework/boot/loader/JarLauncher
org/springframework/boot/loader/ExecutableArchiveLauncher
org/springframework/boot/loader/Launcher
org/springframework/boot/loader/LaunchedURLClassLoader
org/springframework/boot/loader/archive/Archive
org/springframework/boot/loader/archive/JarFileArchive
org/springframework/boot/loader/jar/JarFile
org/springframework/boot/loader/jar/CentralDirectoryVisitor
org/springframework/boot/loader/data/RandomAccessData
org/springframework/boot/loader/jar/FileHeader

もし、ロードされたくないようなクラスがある場合は、このリストから取り除きます。

リストを取得する処理が入る場合は、ロードされるクラスのスキャンニングが入るため、若干処理が遅くなります。


アーカイブファイルの取得

次にアーカイブファイルを取得します。このときの java コマンドはアプリケーションを起動しません。

$ ./gradlew  runBootJar -PdumpApp

> Task :showStartTime 
start 2018-04-01T20:08:10.186895

> Task :runBootJar 
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 2131
    instance classes   =  2053
    obj array classes  =    70
    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:      8536 [  0.0% of total] out of     12288 bytes [ 69.5% used] at 0x0000000800000000
rw  space:   5943264 [ 19.0% of total] out of   5943296 bytes [100.0% used] at 0x0000000800003000
ro  space:  11971160 [ 38.4% of total] out of  11972608 bytes [100.0% used] at 0x00000008005ae000
md  space:      6160 [  0.0% of total] out of      8192 bytes [ 75.2% used] at 0x0000000801119000
od  space:  11275744 [ 36.1% of total] out of  11276288 bytes [100.0% used] at 0x000000080111b000
st0 space:    835584 [  2.7% of total] out of    835584 bytes [100.0% used] at 0x00000007bfe00000
st1 space:   1048576 [  3.4% of total] out of   1048576 bytes [100.0% used] at 0x00000007bff00000
oa0 space:    102400 [  0.3% of total] out of    102400 bytes [100.0% used] at 0x00000007bfd00000
total    :  31191424 [100.0% of total] out of  31199232 bytes [100.0% used]

> Task :showEndTime 
start 2018-04-01T20:08:10.186895
end 2018-04-01T20:08:11.906736
time: 1719 ms


BUILD SUCCESSFUL in 4s
6 actionable tasks: 3 executed, 3 up-to-date

これによって、アーカイブファイルが作られます(この例では build/cds/app.jsa)。また、ログの意味を文字通りに解釈するなら、 2131 個のクラスがアーカイブされたようです。実際にファイルを確認してみます。

$ ls -l build/cds/
total 61080
-r--r--r--+ 1 mike  staff  31203328  4  1 20:08 app.jsa
-rw-r--r--+ 1 mike  staff     69515  4  1 19:54 list.txt

アーカイブファイルを読み込んでアプリケーションを起動する

では、作成したアーカイブファイルを使って、 Spring Boot アプリケーションを起動してみます。

 $ ./gradlew  runBootJar -PrunApp
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8

> Task :showStartTime 
start 2018-04-01T20:11:56.633207

> Task :runBootJar 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.0.RELEASE)

2018-04-01 20:12:03.401  INFO 8564 --- [           main] com.example.App                          : Starting App on mac.local with PID 8564 (/path/to/project/build/libs/spring-app-cds-sample-0.0.1-SNAPSHOT.jar started by mike in /path/to/project)
2018-04-01 20:12:03.409  INFO 8564 --- [           main] com.example.App                          : No active profile set, falling back to default profiles: default

... 中略

> Task :showEndTime 
start 2018-04-01T20:11:56.633207
end 2018-04-01T20:12:06.830557
time: 10197 ms

... 以下省略

全体の時間は 10197 ms となり、指定しなかったとき(10322 ms)に比べて、 125 ms ほど速くなりました!…あれ、いや、これ誤差ですよね…??!?!


問題と解決

さて、起動時間がほとんど変わらない理由を考えます。先ほどの list.txt をよく見てみます。

org/springframework/boot/loader/JarLauncher
org/springframework/boot/loader/ExecutableArchiveLauncher
org/springframework/boot/loader/Launcher

といったクラスがアーカイブされていますが、これらを見てわかるかもしれませんが、 bootJar によって生成される jar ファイルは一般的な fat jar とは異なり、アプリケーションのクラスがそのまま入っているのではなく、 アプリケーションの jar が中に入っています。確認してみます。

$ unzip -Z1 build/libs/spring-app-cds-sample-0.0.1-SNAPSHOT.jar
org/
org/springframework/
org/springframework/boot/
org/springframework/boot/loader/
org/springframework/boot/loader/data/
org/springframework/boot/loader/util/
org/springframework/boot/loader/archive/
org/springframework/boot/loader/jar/
org/springframework/boot/loader/data/RandomAccessDataFile.class
org/springframework/boot/loader/util/SystemPropertyUtils.class
org/springframework/boot/loader/archive/Archive$EntryFilter.class
org/springframework/boot/loader/archive/ExplodedArchive$FileEntry.class
org/springframework/boot/loader/archive/Archive.class
org/springframework/boot/loader/archive/Archive$Entry.class
org/springframework/boot/loader/Launcher.class

... 中略

BOOT-INF/classes/com/example/App$Foo.class
BOOT-INF/classes/com/example/App.class

... 中略

BOOT-INF/lib/spring-boot-starter-webflux-2.0.0.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-actuator-2.0.0.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-data-jpa-2.0.0.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-thymeleaf-2.0.0.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-validation-2.0.0.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-data-redis-reactive-2.0.0.RELEASE.jar
... 以下省略

つまり、どんなにたくさん依存ライブラリーを使っても bootJar タスクで作成した jar ファイルを Class Data Sharing で起動してもあまり効果がないことがわかります。

そこで、 bootRun のようなラウンチャー経由で起動するわけではない形で Spring Boot アプリケーションを起動したいのですが、前述の通り bootRun に Class Data Sharing のオプションをつけて起動すると、どうしてもアーカイブファイルのパスがうまく認識されません。正確には次のようなコマンドで bootRun は起動されるのですが、アプリケーションのクラスがあるディレクトリーをアーカイブファイルのディレクトリーと認識してしまい、アーカイブファイルを出力してくれません。

java \
  -XX:+UnlockCommercialFeatures \
  -XX:+UseAppCDS \
  -Xshare:dump \
  -XX:SharedClassListFile=classes.txt \
  -XX:SharedArchiveFile=archive.jsa \
  -cp /path/to/project/build/classes/java/main:/path/to/.gradle/foo/bar/spring-foo-bar-1.0.0.jar \
  com.sample.App

したがって、 bootRun とほぼ同様のコマンドで、コンパイルされたアプリケーションをクラスファイルのままではなく、一度 jar に固めて起動するようなタスクを作る必要があります(。もちろん、普通に jar ファイルを作って、それを起動するのでも構いませんが、 jar タスクは Spring Boot plugin によって上書きされているので、あらためて作る必要があります)。

というわけで、そのようなタスクを作り、そのタスク名を cdsBootRun(実装、スクリプトは省略) とします。


再確認

では cdsBootRun を使って、起動してみます。

クラスリストの取得

$ ./gradlew cdsBotRun -PlistApp
> Task :showStartTime 
start 2018-04-02T23:00:45.980764

> Task :cdsBootRun 
skip writing class com/sun/proxy/$Proxy0 from source __JVM_DefineClass__ to classlist file
skip writing class com/sun/proxy/$Proxy1 from source __JVM_DefineClass__ to classlist file

... 中略

Action:

Consider defining a bean of type 'com.example.App$Foo' in your configuration.

> Task :showEndTime 
start 2018-04-02T23:00:45.980764
end 2018-04-02T23:00:55.548887
time: 9568 ms

クラスリストを取得するようにJVMパラメーターを調整して実行し、クラスファイルリスト list.txt の中身を確認します。

$ grep spring build/cds/list.txt | head -10
org/springframework/boot/SpringApplication
org/springframework/boot/ApplicationArguments
org/springframework/context/ApplicationContext
org/springframework/core/env/EnvironmentCapable
org/springframework/beans/factory/ListableBeanFactory
org/springframework/beans/factory/BeanFactory
org/springframework/beans/factory/HierarchicalBeanFactory
org/springframework/context/MessageSource
org/springframework/context/ApplicationEventPublisher
org/springframework/core/io/support/ResourcePatternResolver

spring を含むクラスの先頭10行を取得しましたが、先ほどのクラスファイルリストとは異なっていることがわかると思います。これはかなり期待できます!

アーカイブファイルの取得

アーカイブファイルを取得します。

$ ./gradlew cdsBootRun -PdumpApp

> Task :showStartTime 
start 2018-04-02T23:06:08.353299

> Task :cdsBootRun 
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 ...
Preload Warning: Cannot find com/sun/proxy/$Proxy4
Preload Warning: Cannot find org/springframework/core/$Proxy5
Preload Warning: Cannot find org/springframework/core/$Proxy6
Preload Warning: Cannot find com/sun/proxy/$Proxy7
Preload Warning: Cannot find com/sun/proxy/$Proxy8

... 中略

Rewriting and linking classes: done
Number of classes 6389
    instance classes   =  6297
    obj array classes  =    84
    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:      9544 [  0.0% of total] out of     12288 bytes [ 77.7% used] at 0x0000000800000000
rw  space:  15875096 [ 20.8% of total] out of  15876096 bytes [100.0% used] at 0x0000000800003000
ro  space:  29298616 [ 38.4% of total] out of  29298688 bytes [100.0% used] at 0x0000000800f27000
md  space:      6160 [  0.0% of total] out of      8192 bytes [ 75.2% used] at 0x0000000802b18000
od  space:  27230536 [ 35.7% of total] out of  27234304 bytes [100.0% used] at 0x0000000802b1a000
st0 space:    446464 [  0.6% of total] out of    446464 bytes [100.0% used] at 0x00000007bfc00000
st1 space:   3145728 [  4.1% of total] out of   3145728 bytes [100.0% used] at 0x00000007bfd00000
oa0 space:    221184 [  0.3% of total] out of    221184 bytes [100.0% used] at 0x00000007bfb00000
total    :  76233328 [100.0% of total] out of  76242944 bytes [100.0% used]

> Task :showEndTime 
start 2018-04-02T23:06:08.353299
end 2018-04-02T23:06:28.96977
time: 20616 ms


BUILD SUCCESSFUL in 22s
5 actionable tasks: 3 executed, 2 up-to-date

さて、このコマンドの結果にも大きな変更がありました。最初に試したときは 2131 個のファイルがアーカイブされたのに対して、今度は 6389 個のファイルがアーカイブされたようです。

$ ls -l build/cds/
total 149600
-r--r--r--+ 1 mike  staff  76271616  4  2 23:06 app.jsa
-rw-r--r--+ 1 mike  staff    323139  4  2 23:00 list.txt

また、ファイルの大きさを確認しても、最初のときは 31,203,328 だったのに対して、 76,271,616 に増えています。これは本当に期待できそうですよ!

アーカイブファイルを読み込んでアプリケーションを起動する

ではアーカイブファイルを読み込んでアプリケーションを起動してみます。

$ ./gradlew cdsBootRun -PrunApp

> Task :showStartTime 
start 2018-04-02T23:14:33.147381

> Task :cdsBootRun 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.0.RELEASE)

2018-04-02 23:14:39.305  INFO 10455 --- [           main] com.example.App                          : Starting App on mac.local with PID 10455 (/path/to/project/build/customJar/spring-app-cds-sample-0.0.1-SNAPSHOT.jar started by mike in /path/to/project)
2018-04-02 23:14:39.307  INFO 10455 --- [           main] com.example.App                          : No active profile set, falling back to default profiles: default

... 中略

> Task :showEndTime 
start 2018-04-02T23:14:33.147381
end 2018-04-02T23:14:41.733068
time: 8585 ms

... 以下略

結果としては次のようになりました。

  • 全体の時間 : 8585 ms

先ほどと比べて…と言いたいところですが、起動するものが変わったので最初にCDSを使わなかったものを比較対象にするのは間違っていますので、あらためてCDSを使わない場合の起動時間を調べてみます。

$ ./gradlew cdsBootRun

> Task :showStartTime 
start 2018-04-02T23:22:34.634671

> Task :cdsBootRun 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.0.RELEASE)

2018-04-02 23:22:40.818  INFO 10507 --- [           main] com.example.App                          : Starting App on mac.local with PID 10507 (/path/to/project/build/customJar/spring-app-cds-sample-0.0.1-SNAPSHOT.jar started by mike in /path/to/project)
2018-04-02 23:22:40.822  INFO 10507 --- [           main] com.example.App                          : No active profile set, falling back to default profiles: default

... 中略

> Task :showEndTime 
start 2018-04-02T23:22:34.634671
end 2018-04-02T23:22:43.906533
time: 9271 ms

... 以下略

CDSを使わなかった場合の起動時間は次のようになりました。

  • 全体の時間 : 9271 ms

比較してみると、 CDS を使った場合の方が 686 ms ほど速くなりました!

条件 時間(ms)
CDS使った場合 8585
CDS使わない場合 9271