今日は JVM のクラスローダー機構について紹介します。主な内容は以下の通りです:
- 概要
- クラスロードのタイミング
- クラスロードのプロセス
- クラスローダー
- クラスローダーの分類
- 親委任モデル
概要#
JVM はバイトコード(.class)ファイルをメモリにロードし、データの検証、解析、初期化を行い、最終的に JVM が直接使用できる Java 型を生成します。これが JVM のクラスロード機構です。
Java におけるさまざまなタイプのロード、接続、初期化プロセスはプログラムの実行中に完了します。この方法はクラスロード時にいくつかのパフォーマンスオーバーヘッドをもたらしますが、高い柔軟性を持っています。Java の動的拡張の言語特性は、実行時に動的にロードおよびリンクされるという特徴に依存しています。プラグイン技術では、カスタムクラスローダーを使用してリソースのロードと置き換えを実現しています。これが Java 言語の実行時におけるクラスロードの特性です。
クラスロードのタイミング#
クラスが JVM メモリにロードされてから、JVM メモリからアンロードされるまでの間、クラスロードのライフサイクルは以下の図のようになります:
ロード、検証、準備、初期化、アンロードの 5 つの段階の順序は決まっていますが、クラスの解析は必ずしもそうではなく、初期化の後に行われることもあります。これは Java 言語のランタイムバインディングをサポートするためです。クラスロードの全過程において、各段階は前の段階によってトリガーされます。
JVM 仕様ではクラスの初期化段階が規定されていますが、ロードの段階には制約がなく、具体的には JVM の実装に依存します。もちろん、ロード、検証、準備は初期化の前に完了する必要があります。
では、どのような場合にクラスが初期化を開始するのでしょうか。JVM は以下の状況でクラスを初期化する必要があると厳格に規定しています:
- new、getstatic/putstatic、invokestatic 命令に遭遇したとき、そのクラスが初期化されていない場合は、クラスを初期化する必要があります。上記の命令はそれぞれ、new キーワードを使用してオブジェクトをインスタンス化すること、静的プロパティを読み取るまたは設定すること、静的メソッドを呼び出すことに対応しています。具体的には
javap
コマンドを使用してバイトコードファイルの実装を確認できます。 - java.lang.reflect を使用してクラスにリフレクション呼び出しを行うとき、そのクラスが初期化されていない場合は、クラスを初期化する必要があります。
- クラスを初期化する際に、その親クラスがまだ初期化されていない場合は、まずその親クラスを初期化します。
- JVM が起動するとき、ユーザーが起動する主クラスを指定します。例えば、main メソッドを持つクラスがある場合、JVM は最初にこのクラスを初期化します。
- JDK 1.7 の動的言語サポートを使用する場合、java.lang.invoke.MethodHandle インスタンスの最終的な解析結果が REF_getStatic、REF_putStatic、REF_invokeStatic であり、これらのハンドルに対応するクラスが初期化されていない場合は、まずそれを初期化する必要があります。MethodHandle はリフレクションの別の形式と考えることができます。
クラスロードのプロセス#
以下にクラスロードのいくつかの段階について具体的に説明します。
ロード#
class ファイルはクラスローダーによってそのバイトコード内容をメモリにロードし、この静的データをメソッド領域のランタイムデータ構造に変換し、ヒープメモリにその class ファイルに対応する java.lang.Class オブジェクトを生成します。この Class オブジェクトはメソッド領域のクラスデータにアクセスするためのエントリポイントです。
JVM 仕様では class ファイルの出所は規定されていません。以下に例を示します:
- zip パッケージから取得し、最終的に jar、war 形式の基礎となります。
- ネットワークから取得し、典型的なアプリケーションは Applet です。
- 実行時に生成され、典型的なアプリケーションは動的プロキシ技術で、java.lang.reflect.Proxy では ProxyGenerator.generateProxyClass を使用して特定のインターフェースの形状の Proxy のプロキシクラスのバイナリバイトストリームを生成します。
- その他のファイル生成、データベースから取得など。
クラスのロード段階は後のリンク段階と交互に行われ、明確な境界はありません。ロード段階がまだ完了していなくても、リンク段階が始まることがありますが、2 つの段階の開始時間は依然として固定の前後関係を保持しています。
リンク#
リンクは検証、準備、解析の 3 つの段階を含みます。
- 検証:class ファイルのバイトストリームに含まれる情報が現在の仮想マシンの要求に合致し、仮想マシン自身の安全を害さないことを確認します。全体的に見ると、検証段階は主にファイル形式の検証、メタデータの検証、バイトコードの検証、シンボル参照の検証を含みます。具体的な検証内容は Java 仮想マシン仕様を参照してください。
- 準備:正式にクラス変数にメモリを割り当て、クラス変数の初期値を設定します。この初期値は一般にデータ型の初期値であり、実際のコードで初期化された値ではありません。例えば、int の初期値は 0 です。これらのクラス変数が使用するメモリはメソッド領域で割り当てられ、クラス変数とは static キーワードで修飾された変数を指します。
- 解析:JVM は定数プール内のシンボル参照を直接参照に置き換えます。ここでのシンボル参照は前述の検証段階で言及されたシンボル参照の検証におけるシンボル参照です。
初期化#
クラス初期化段階はクラスロード段階の最後のステップです。前のロード段階、リンク段階はユーザー定義のクラスローダーが関与する以外は、すべて JVM によって完了します。初期化段階では実際に Java コード、つまりバイトコードが実行されます。クラスの初期化については以下の点を理解しておくと良いでしょう:
- 初期化段階はクラスコンストラクタ() メソッドを実行するプロセスです。
- () メソッドはコンパイラによってクラス内のすべてのクラス変数の代入アクションと静的ステートメントブロック static {} 内のステートメントを統合して生成されます。コンパイラの収集順序はソースコード内のステートメントの順序と一致します。静的ステートメントブロック内では、それ以前に定義された変数のみをアクセスでき、後に定義された変数はコピーすることはできますが、アクセスすることはできません。
- クラスを初期化する際に、親クラスがまだ初期化されていない場合は、まず親クラスを初期化します。
- JVM はマルチスレッド環境でクラスの() メソッドが正しくロックおよび同期されることを保証します。
- Java クラスの静的ドメインにアクセスする際、実際にそのクラスが宣言されている場合のみ初期化されます。
クラスローダー#
その名の通り、クラスローダー(class loader)は Java クラスを JVM にロードするためのもので、すべてのクラスローダーはjava.lang.ClassLoader
クラスのインスタンスです。前述のように、クラスのロード段階ではクラスローダーを介して class ファイルがロードされます。つまり、クラスの完全修飾名を使用してこのクラスを定義するバイナリバイトストリームを取得できます。このアクションのコード実装がクラスローダーの実装です。
任意のクラスについて、そのクラスローダーとこのクラス自体を一緒に JVM 内での一意性を確立する必要があります。各クラスローダーは独立したクラス名空間を持っており、つまり、同じクラスが異なるクラスローダーによってロードされると、もはや等しくありません。
クラスローダーの分類#
JVM の観点から見ると、異なるクラスローダーは 2 種類しか存在しません:
- ブートストラップクラスローダー(Bootstrap ClassLoader):一般に C++ 言語で実装され、具体的には JVM によって実装されます。
- その他のクラスローダー:Java 言語で実装され、JVM の外に独立しており、すべて
java.lang.ClassLoader
のインスタンスです。例えば、Android の DexClassLoader などです。
Java 開発者の観点から見ると、クラスローダーは 3 つのカテゴリに分けることができます:
-
ブートストラップクラスローダー(Bootstrap ClassLoader):JAVA_HOME\lib 下のライブラリをロードする責任があり、または - Xbootclasspath パラメータで指定されたパスにあり、JVM が認識するもの(ファイル名のみで認識します。rt.jar のように、名前が一致しないライブラリは lib ディレクトリにあってもロードされません)。ブートストラップクラスローダーは JAVA プログラムから直接使用することはできません。
-
拡張クラスローダー(Extension ClassLoader):このクラスローダーは
sun.misc.Launcher$ExtClassLoader
によって実装され、JAVA_HOME\lib\ext 下のクラスをロードする責任があり、または java.ext.dirs システム変数で指定されたパスにあるライブラリをロードします。拡張クラスローダーは直接使用できます。 -
アプリケーションクラスローダー(Application ClassLoader):このクラスローダーは
sun.misc.Launcher$AppClassLoader
によって実装され、このクラスローダーは ClassLoader の getSystemClassLoader () メソッドの戻り値であり、一般にシステムクラスローダーとも呼ばれ、ユーザークラスパス(ClassPath)で指定されたライブラリをロードする責任があります。開発者はこのクラスローダーを直接使用できます。アプリケーション内で独自のクラスローダーを定義していない場合、これがプログラム内のデフォルトのクラスローダーです。
親委任モデル#
まず、上記のクラスローダーの関係を見てみましょう:
上の図に示されているクラスローダー間のこの階層関係は、クラスローダーの親委任モデル(Parents Delegation Model)と呼ばれます。親委任モデルは、最上位のブートストラップクラスローダーを除いて、他のクラスローダーはそれぞれ自分の親クラスローダーを持つべきであると要求します。ここでのクラスローダー間の親子関係は一般に継承(Inheritance)によって実現されることはなく、すべて組み合わせ(Composition)関係を使用して親ローダーのコードを再利用します。この方法は強制的な制約モデルではなく、Java の設計者が開発者に推奨するクラスローダーの実装方法です。
では、親委任モデルの作業フローはどのようなものでしょうか?
クラスローダーがクラスロードのリクエストを受け取ると、最初にそのクラスをロードするのではなく、そのクラスロードのリクエストを親クラスローダーに委任します。これを繰り返し、最終的にすべてのクラスロードのリクエストはブートストラップクラスローダーに委任されます。親クラスローダーがそのクラスのロードを完了できない場合にのみ、子クラスローダーが自分でロードを試みます。ロードプロセスは以下のようになります:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 1. すでにクラスがロードされているか確認
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. まだロードされていない場合、親クラスのクラスローダーを呼び出してロード
c = parent.loadClass(name, false);
} else {
// 3. 親クラスのクラスローダーが存在しない場合、直接ブートストラップクラスローダーを使用してロード
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// クラスが見つからない場合、非nullの親クラスローダーからClassNotFoundExceptionがスローされます
}
if (c == null) {
// まだ見つからない場合、findClassを呼び出してクラスを見つけます。
long t1 = System.nanoTime();
// 4. 親クラスまたはブートストラップクラスローダーがそのクラスをロードしていない場合、子クラスローダーのfindClassメソッドを呼び出してクラスをロード
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
JDK 1.2 以降、java.lang.ClassLoader には新しい protected メソッド findClass () が追加されました。クラスローダーをカスタマイズする場合は、findClass () メソッドを直接実装すればよく、loadClass () メソッドをオーバーライドする必要はありません。なぜなら、loadClass () メソッドは最終的に findClass () メソッドを呼び出すからです。このようにしてカスタマイズされたクラスローダーは親委任ルールに準拠します。
前述の JVM クラスロード機構およびクラスローダーに関する知識を紹介しました。クラスローダーは Java の動的拡張特性をうまくサポートしており、Android でも使用されています。プラグイン技術で使用される PathClassLoader や DexClassLoader はすべて ClassLoader の間接的なサブクラスであり、class ファイルの出所に制限を設けないことによって、アプリのプラグイン化を実現できます。