🐔

Java アプリ用のコンテナイメージ作るんなら jlink コマンドでカスタム JRE 作っとけ

2021/12/21に公開

この記事は 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/*.jmodlib/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.jsalib/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