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

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

クラスローダ

基本的な仕組み

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

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

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

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

また,クラスローダは基本的に子にはロードを委譲しません.これは結構重要なポイントです.例えば,クラスローダAの検索範囲にクラスaがあり,その子クラスローダであるクラスローダBの検索範囲にクラスbがあるとします。この場合,クラス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