Java アプリ用のコンテナイメージ作るんなら jlink コマンドでカスタム JRE 作っとけ
この記事は Java Advent Calendar 2021 の第 9 日目が空いていたので、急遽ぶっこんだ記事である。
何の話?
出オチ。タイトルのまんま。
最近は JRE 配布してくんないけど、かと言って JDK はそのままじゃデカいので、コンテナイメージ作るんなら jlink
コマンドでカスタム JRE 作っとけよ、と言う話。
あ、当然だが Java 9 以降の話。
jlink
コマンドって何?
Java 9 からモジュールシステムが導入されたせいで(?)、JDK 自身もモジュールに分割されるようになった。
そうすると、アプリケーションの実行時には JDK のうち実際にそのアプリケーションが必要なモジュールだけ抽出できればスッキリ(?)するよね、と言う事でそれをやるのが jlink
コマンドだ。まぁ細かいことを言うと(細かいか?)jlink
コマンドは JDK 謹製モジュールから抽出できるだけじゃなくて、ユーザの作ったモジュールも一緒にまとめることができるし、ランチャ作ったりもできる。
で、jlink
コマンドでまとめたものを「カスタム JRE」と言ったりする。(多分)
カスタム JRE?JIMAGE?
余談だが、カスタム JRE 全体のことを JIMAGE と呼んでいるサイトがあるようだが、これは恐らく正しい用語の使い方ではない。
JIMAGE とは Java 9 で新たに導入されたファイルフォーマットの事で、jlink
コマンドで作られるカスタム JRE のうちの一つのファイル lib/modules
がそのフォーマットで作成されている。(正確に言えば、このファイルは JRE だけではなく JDK にも存在する)
こいつは Java 8 までで言うと lib/rt.jar
とかに当たるもので、カスタム JRE(あるいは JDK)のクラスファイルを格納している。つまり、こいつがクラスファイルの実体でカスタム JRE のクラスファイルは(と言うか JDK のクラスファイルも)jar 形式では無いという事だ。
じゃあ、カスタム JRE に含まれているクラスファイルの一覧を見たりクラスファイルを抽出したりすることは出来ないのかと言うと、JIMAGE のファイルを操作するための、その名も jimage
なるコマンドがあるので、大丈夫だ。ただし、jimage
ではクラスファイルを追加・更新・削除することは出来ないし(jlink
でやれ、と言う事だ)、一部のクラスファイルだけを抽出することもできない(全クラスファイルが出てくる)。
別に JDK そのまま使ってればいんじゃね?
まぁその通りなんだが、状況によっては JDK のサイズを小さくしたいことがある。アプリケーションを Java の実行環境ごとダウンロードさせて使わせる場合なんかは、小さいに越したことは無い。
コンテナは正にこれで、コンテナイメージは小さければ小さいほど良い。(だよね?)
で、DockerHub にある OpenJDK のオフィシャルイメージとか見てみると分かるが JDK はめっさデカい。
⇓は openjdk:17-jdk-bullseye-slim
のレイヤーサイズ。
サイズ | サイズ(圧縮) | |
---|---|---|
OS 本体レイヤ | 80 MB | 28.65 MB |
追加パッケージレイヤ | 4.9 MB | 1.49 MB |
JDK レイヤ | 322 MB | 177.5 MB |
合計 | 406.9 MB | 207.64 MB |
圧縮サイズでは JDK が OS の 6 倍ぐらい食ってる。
あ、ちなみに、サイズは実際のファイルサイズで、圧縮ってのは配布用コンテナイメージとして圧縮された状態でのサイズ。
あと、「追加パッケージレイヤ」ってのは Java 動かすのに必要ってことで OS のパッケージマネージャで追加導入されたパッケージを指す。
openjdk:17-jdk-alpine3.14
だともっと絶望的。
サイズ | サイズ(圧縮) | |
---|---|---|
OS 本体レイヤ | 5.6 MB | 2.68 MB |
追加パッケージレイヤ | 2.4 MB | 906.68 KB |
JDK レイヤ | 318 MB | 178.14 MB |
合計 | 326 MB | 181.71 MB |
Alpine Linux が己が身を削って 1 バイトを捻出してるのに(?)、その努力を踏みにじるようなサイズだ。こうなると最早 OS 自身のサイズは誤差と言っていい。
と言うわけで、Java のアプリケーションでイメージを小さくしたかったら、jlink
コマンドによるカスタム JRE の作成は必須だ。
JDK は何でそんなにデカいの?
一言で言ってしまえば、実行に必要無いモノが含まれているからだ。
こう言ってしまうと「あ~、まぁそりゃあ要らないモジュール削ればサイズも減るだろ」と誤解する人もいるかもしれないが、ここで言っているのはそういう話じゃない。要らないモジュールとか関係なくホントに必要無いモノが含まれているのだ。
具体的に言うと、使われないくせに特にデカくて邪魔なのは jmods/*.jmod
と lib/src.zip
、それから lib/server/classes_nocoops.jsa
の 3 つだ。残念ながら四天王じゃない。
サイズ | |
---|---|
jmods/*.jmod |
77 MB |
lib/src.zip |
51 MB |
lib/server/classes_nocoops.jsa |
12 MB |
合計 | 140 MB |
なので、⇑では「jlink
コマンドによるカスタム JRE の作成は必須だ」とは言ったが、「アプリはまだモジュール化してねぇし、JDK の何のモジュール使ってるかも分かんね~よ!」とか言う向きは、これらのファイルを削除するだけでも結構小さくなる。
あ、当たり前だが、オフィシャルイメージをベースに RUN jlink ...
とか RUN rm ...
とかやるだけじゃ減らんよ。オフィシャルイメージ使いつつ小さくしたいんならマルチステージビルドとかで⇓みたいな感じで頑張ってくれ。(これは openjdk:17-jdk-slim-bullseye
の丸パクリ)
FROM openjdk:17-jdk-slim-bullseye AS builder
RUN jlink --add-modules .... --output /usr/local/jre
# 削除するだけなら⇓
#RUN rm -rf $JAVA_HOME/{jmods, lib/src.zip, lib/server/classes_nocoops.jsa}
...
FROM debian:bullseye-slim
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
# utilities for keeping Debian and OpenJDK CA certificates in sync
ca-certificates p11-kit \
; \
rm -rf /var/lib/apt/lists/*
ENV JAVA_HOME /usr/local/jre
ENV PATH $JAVA_HOME/bin:$PATH
# Default to UTF-8 file.encoding
ENV LANG C.UTF-8
COPY --from builder /usr/local/jre /usr/local/jre
# 削除するだけなら⇓
#COPY --from builder /usr/local/openjdk-17 /usr/local/jre
# "jshell" is an interactive REPL for Java (see https://en.wikipedia.org/wiki/JShell)
CMD ["jshell"]
ちなみに、めんどくさくて圧縮サイズは見てない。申し訳 nothing。(Docker のコンテナイメージってなにで圧縮されてるんだっけ?)
jmod って何?
コンパイル時、および、リンク時(jlink
コマンドを使う時)用のモジュールファイル。入れ物としては単なる zip ファイル(つまり jar と一緒)なのだが、ディレクトリ構成が jar とちょっと違っていて、実行ファイルや共有ライブラリ等のバイナリファイルその他も格納する事を想定している。
で、「コンパイル時、および、リンク時用のモジュールファイル」と書いているように、実行時には使われない。つまり、実行時には単なる邪魔者でしかない。
src.zip って何?
いや、みんな知ってるとは思うが、一応。
JDK のライブラリ達の Java ソースファイルをがっつり固めたモノ。開発中、特にデバッグ時とかにはありがたい存在だが、当然のことながら実行時には不要。
コンテナ内で jar xvf
してソース見始めたりしないよね?(する人おる?)
lib/server/classes_nocoops.jsa って何?
lib/server/classes_nocoops.jsa
は要らないと言ったな。あれはウソだ。
正確には、lib/server/classes.jsa
と lib/server/classes_nocoops.jsa
の少なくともいずれか一方、もしくはその両方が要らない、が正解だ。
この lib/server/classes*.jsa
なる物は、いつの頃からか(JDK 12 あたりか?)JDK にくっ付いてくるようになった default CDS archive って言うシロモノである。
実際にどっちが要らないのか(あるいは両方要らないのか)については別記事「jlink コマンドでカスタム JRE 作った時は java -Xshare:dump で default CDS archive 作っとけ」を見て欲しいが、10 MB 以上あるので使われないんなら入れない方がいいだろう。
jlink コマンド使ってもこいつら削除しないとダメ?
jlink
でカスタム JRE を作成すると、こいつらは付いてこない。したがってわざわざ削除する必要は無い。
しかも、モジュールも必要なものに絞ることができるので、jlink
コマンドを使う事をお勧めする。
ちなみに、別記事「jlink コマンドでカスタム JRE 作った時は java -Xshare:dump で default CDS archive 作っとけ」に書いたが、カスタム JRE 作った時は default CDS archive を作成した方が良いと思う。
jlink でモジュール絞ってもデカいんだが?
jlink
コマンドには --compress
なるオプションがあって、以下のようなレベルを指定できる。
レベル | 意味 |
---|---|
0 | No compression |
1 | Constant string sharing |
2 | ZIP |
デフォルトでは 0
、つまり無圧縮なのだが、1
とか 2
にすると小さくなる。
で、何が圧縮されているかと言うと、JIMAGE、つまり lib/modules
である。jimage
コマンドの list
サブコマンドにオプション --verbose
を指定すると、⇓みたいに格納されている各ファイルの元のファイルサイズと圧縮後のファイルサイズが表示される。
jimage: /usr/local/jre/lib/modules
Module: java.base
Offset Size Compressed Entry
12229561 41 0 META-INF/services/java.nio.file.spi.FileSystemProvider
14606325 574 264 com/sun/crypto/provider/AESCipher$AES128_CBC_NoPadding.class
14607909 574 264 com/sun/crypto/provider/AESCipher$AES128_CFB_NoPadding.class
14605533 574 264 com/sun/crypto/provider/AESCipher$AES128_ECB_NoPadding.class
14607117 574 264 com/sun/crypto/provider/AESCipher$AES128_OFB_NoPadding.class
...
ちなみに、圧縮後のファイルサイズが 0
になっているものは、無圧縮のまま格納されている、と言う事で、間違っても 0
バイトに圧縮されたという事ではない。
参考までに、JDK のモジュール全部入りの場合のサイズを見てみた。
レベル |
lib/modules のサイズ |
---|---|
0 | 126 MB |
1 | 90 MB |
2 | 57 MB |
結構サイズ違うな。
あ、こいつもめんどくさくて圧縮サイズは見てない。申し訳 nothing
ちなみに、ZIP 圧縮なんかするとロード時の負荷高いんじゃね?と思ったりするが、これも未検証。検証してから使って欲しい。重ね重ね申し訳 nothing
ただまぁ jar だって zip 圧縮なんだから気にするほどのことは無いのか?知らんけど…
Discussion