【クラスローダ】JVMが読み込むクラスを見つける仕組み【パッケージ】

NoClassDefFoundError や ClassNotFoundException に遭遇した時など,Java で書くならクラスローディングについてある程度知っておいたほうがよいケースというのはあるかなあと思い調べたときのメモ.なんとなくの概要.

クラスローダ

基本的な仕組み

Java プログラムは基本的に,複数のファイルに分かれた沢山のクラスによって成り立っています.プログラムを動かすためには,これらのクラスを JVM が読み込まなければなりません.Java では JVM のクラスローダと呼ばれる機構がこの処理を担います.クラスローダは,読み込むべきクラスを判断し,ストレージから当該クラスファイルを探し出し,ファイルを開いてクラスをロードします.

クラスロードの戦略としては,オンデマンドな感じです.プログラムの開始時に全てのクラスを読み込むのではなく,実行中に必要になったタイミングで必要になったクラスの検索→ロードをします.

クラスローダは役割ごとに複数存在していて,それらが階層構造(親子関係)をとっています.役割といってもやることは同じなのですが,クラスをロードする場所がそれぞれ異なる形です.あるクラスローダはどこどこのパスからクラスをロードするのを担当して,あるクラスローダは別のパスからロードするのを担当するという感じで.

で,基本的には,親の担当範囲で見つけられるクラスなら親にロードしてもらう,親がロードできなければ子がロードするという風に,上位のクラスローダから順に特定のクラスのロードを試みていきます.ロードの起点となるのは,実行されているクラスをロードしたクラスローダです.起点となったクラスローダは親がいれば親にロードを任せ,その親が更にその親に任せていきます.この仕組みは委譲(delegate)と呼ばれます.

また,クラスローダは基本的に子に委譲しません.これは結構重要なポイントです.例えば,クラスローダAによってロードされたクラスAを実行する際,クラスAが必要とする他のクラスのロードの起点となるのはクラスローダAです.すなわち,クラスA実行時には,その子クラスローダであるクラスローダBがロードを担当する範囲にあるクラスBをロード出来ないということを意味します.起点となったクラスローダより階層的に下位に存在するクラスローダは使われず,その下位クラスローダにしかロードできないクラスはロードできないのです(が,このスタイルが問題になるケースも存在するということで,委譲の順番をあえて変更しているサーバ等も一応存在).

このように,ロードに優先順位というか,方向性をもたせることで,複数のクラスローダが同じクラスを重複してロードしたりして競合が発生したりすることを防げます.このような仕組みになっているので,とあるクラスが,起点となるクラスローダの下位クラスローダ配下にしか存在しないクラスに依存する,という設計はよろしくないでしょう.

また,クラスローダは階層構造の横方向にも委譲しません.すなわち,兄弟関係にあるクラスローダ同士はお互いに影響し合わない仕組みになっています.このようになっていると,単一のサーバにデプロイされる複数のアプリケーションごとに兄弟関係になるクラスローダを作ることで,それぞれのアプリケーションでバージョンが違うクラスを読み込んだりできます.

基本となるクラスローダ

で,基本的なクラスローダは,ブートストラップクラスローダ,拡張クラスローダ,システムクラスローダの3つです.これらは挙げた順に親子関係になっています.

ブートストラップクラスローダは JVM そのものの実行に必要になるクラスをロードする基礎的なクラスローダです.JDK のコア API に関するクラスなどがこのクラスローダにロードされます.例えば,java.lang.* などがそれに当たります.<JAVA_HOME>/lib にあるような jar とかですね.また,このクラスローダは基本的には JVM 内にネイティブ実装されています.そのおかげで,そもそも最上位のクラスローダは誰がロードするのか問題(パラドックス)を解決できています.

拡張クラスローダは,拡張用ディレクトリ(一般的には <JAVA_HOME>/lib/ext)にあるクラスをロードします.クラスパスを変更することなく基礎的な機能を Java に追加したい場合には,このディレクトリに追加してあげれば簡単に実現できます.

システムクラスローダは,クラスパス(環境変数)で指定している場所のクラスのロードを担当します.普段プログラムを動かすためにクラスパスを追加すると思いますが,その場所のクラスはこいつが読み込んでくれています.また,このシステムクラスローダを親として独自のクラスローダを作成することも可能です(ユーザ定義クラスローダ).ユーザ定義クラスローダを作ることで,アプリケーション単位でクラスローダを割り当て,ライブラリのロード方法を変えたりできます.

ブートストラップクラスローダはネイティブ実装されているのですが,その他のクラスローダは Java オブジェクトとして存在するので,プログラム中でそのインスタンスを取得したりなんてことも簡単にできます.

クラスローディングの流れの大枠

JVM がプログラムを実行中にクラスのロードが必要だと判断すると,クラスローダに処理を依頼します.クラスローダはロードすべきクラスの完全修飾名を基に処理を始めます.完全修飾名とは,com.example.Hoge のようにクラス名とパッケージ名を繋げたものです.クラスローダは自分がどの場所からクラスを探せばよいかは知っていますが,その場所のどのクラスをロードすればよいかは知らないので,この完全修飾名を与えてロードすべきクラスを決定します.

例えば,/usr/local/lib を探すクラスローダがあったとして,JVM がそれに com.example.Hoge クラスをロードしてもらう指示を出した場合,完全修飾名がパスに変換され /usr/local/lib/com/example/Hoge.class をロードしようとします.この際,クラスが見つからない場合には ClassNotFound の例外が投げられることになります.

言い換えると,読み込まれるクラスは,パッケージ名とクラス名,そしてクラスローダの3つによって決定されるということです.完全修飾名が全く同じクラスが別の場所に複数存在していた場合,処理をするクラスローダによってどのクラスが読み込まれるかが変わってきます.また,同名の完全修飾名をもったクラスが複数のクラスローダからロードされ得る場合には,一番最初に発見されたクラスがロードされる事になっています.

クラスが無事発見されると,そのクラスが正しい構造になっているかどうかを検証したり,static フィールドを初期化したり,static イニシャライザを実行したりして,クラスを使える状態にします.

おわり

このように Java ではクラスローダという機構がクラスロードを担当しています.他の言語でのクラスロードの仕組みがどうなっているのか少し気になったので,また暇な時に調べて記事にしようかなーと思います.おわり.

参考

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.3
https://www.ibm.com/developerworks/jp/websphere/library/java/j2ee_classloader/1.html