JVM シリーズ - クラスローダーのメカニズム#
内容整理自:
一、簡述#
仮想マシンはクラスを記述するデータを class ファイルからメモリにロードし、データの検証、変換解析、初期化を行い、最終的に仮想マシンが直接使用できる Java タイプを形成します。これが仮想マシンのクラスローディングメカニズムです。
二、クラスのロードプロセスとライフサイクル#

クラスのロードプロセスは 3 つのステップ(5 つのフェーズ)に分かれています:ロード -> リンク(検証、準備、解析) -> 初期化
ロード#
ロードのプロセスの説明:
- クラスの完全修飾名を使用して.class ファイルを特定し、そのバイナリバイトストリームを取得します。
- バイトストリームが表す静的ストレージ構造をメソッド領域のランタイムデータ構造に変換します。
- Java ヒープ内にこのクラスの java.lang.Class オブジェクトを生成し、メソッド領域内のこれらのデータへのアクセスエントリとして使用します。
リンク#
リンク:検証、準備、解析の 3 つのステップを含みます。
検証#
検証はリンクフェーズの最初のステップであり、Class バイトストリーム内の情報が仮想マシンの要件を満たしていることを確認します。
具体的な検証形式
ファイル形式の検証:バイトストリームが Class ファイル形式の規範に準拠しているかを検証します。例えば:0xCAFEBABE で始まるか、主バージョンと副バージョンが現在の仮想マシンの処理範囲内にあるか、定数プール内の定数がサポートされていない型でないか。メタデータの検証:バイトコードが記述する情報の意味解析を行い(注意:javac コンパイルフェーズの意味解析と比較)、その情報が Java 言語規範の要件を満たしていることを保証します。例えば:このクラスに親クラスがあるか、java.lang.Object 以外。バイトコードの検証:データフローと制御フローの分析を通じて、プログラムの意味が合法で論理的であることを確認します。シンボル参照の検証:解析アクションが正しく実行できることを保証します。
準備#
クラスの静的変数にメモリを割り当て、デフォルト値に初期化します。準備プロセスは通常、クラス情報を格納するための構造を割り当て、この構造にはクラス内で定義されたメンバー変数、メソッド、インターフェース情報などが含まれます。
具体的な行動:
- この時、メモリ割り当ては
クラス変数(static)のみを含み、インスタンス変数は含まれません。インスタンス変数はオブジェクトがインスタンス化される際にオブジェクトと共に Java ヒープに割り当てられます。 - ここで設定される初期値は通常、
データ型のデフォルトのゼロ値(例えば 0、0L、null、false など)であり、Java コード内で明示的に割り当てられたものではありません(明示的に割り当てられた定数は例外です)。
解析#
解析:クラス内の定数プールに対するシンボル参照を直接参照に変換します。
シンボル参照 (Symbolic References): シンボル参照は、参照されるターゲットを記述するために一連のシンボルを使用します。シンボルはターゲットを一意に特定できる任意の形式のリテラルである必要があります。シンボル参照はメモリレイアウトに依存しないため、参照されるオブジェクトがすでにメモリにロードされている必要はありません。さまざまな仮想マシンの実装のメモリレイアウトは異なる場合がありますが、受け入れられるシンボル参照は一貫している必要があります。なぜなら、シンボル参照のリテラル形式は Class ファイル形式で明確に定義されているからです。
直接参照 (Direct References): 直接参照は、ターゲットを指すポインタ、相対オフセット、またはターゲットを間接的に特定できるハンドルを指します。直接参照は仮想マシンの実装のメモリレイアウトに関連しており、同じシンボル参照が異なる仮想マシンで翻訳されると、一般的に直接参照は異なります。直接参照が存在する場合、それはすでにメモリに存在している必要があります。
定数プール内の定数タイプ:
- 定数プール内の定数の数は固定されていないため、定数プールの先頭には u2 型の符号なし整数が配置され、現在の定数プールの容量を格納します。
- 定数プールの各定数はテーブルであり、テーブルの最初の位置には u1 型のフラグ(tag)があり、現在の定数がどの種類の定数タイプに属するかを示します。
| タイプ | tag | 説明 |
|---|---|---|
| CONSTANT_utf8_info | 1 | UTF-8 エンコードされた文字列 |
| CONSTANT_Integer_info | 3 | 整数リテラル |
| CONSTANT_Float_info | 4 | 浮動小数点リテラル |
| CONSTANT_Long_info | 5 | 長整数リテラル |
| CONSTANT_Double_info | 6 | 倍精度浮動小数点リテラル |
| CONSTANT_Class_info | 7 | クラスまたはインターフェースのシンボル参照 |
| CONSTANT_String_info | 8 | 文字列型リテラル |
| CONSTANT_Fieldref_info | 9 | フィールドのシンボル参照 |
| CONSTANT_Methodref_info | 10 | クラス内メソッドのシンボル参照 |
| CONSTANT_InterfaceMethodref_info | 11 | インターフェース内メソッドのシンボル参照 |
| CONSTANT_NameAndType_info | 12 | フィールドまたはメソッドのシンボル参照 |
| CONSTANT_MethodHandle_info | 15 | メソッドハンドルを表す |
| CONSTANT_MethodType_info | 16 | メソッドタイプを識別 |
| CONSTANT_InvokeDynamic_info | 18 | 動的メソッド呼び出し点を表す |
解析アクションは主にクラスまたはインターフェース、フィールド、クラスメソッド、インターフェースメソッド、メソッドタイプ、メソッドハンドル、および呼び出し点修飾子などの 7 種類のシンボル参照に対して行われます。
初期化#
初期化:クラスの静的変数に正しい初期値を与えます。
初期化の目標#
- 宣言されたクラスの静的変数に指定された初期値を初期化すること。
- 静的コードブロックで設定された初期値を初期化すること。
初期化のステップ#
- このクラスがまだロードされていない、またはリンクされていない場合は、最初にこのクラスをロードし、リンクします。
- このクラスの直接の親クラスがまだ初期化されていない場合は、最初にその直接の親クラスを初期化します。
- クラスに初期化文がある場合は、順番に初期化文を実行します。
初期化のタイミング#

ここでの状況 1 の 4 つのバイトコード命令は、Java で最も一般的なシナリオは次のとおりです:
- オブジェクトを new する時
- クラスの静的フィールドを set または get する時(final 修飾子が付いて定数プールに入れられた静的フィールドを除く)
- クラスの静的メソッドを呼び出す時
Java における親クラスと子クラスの初期化順序#
- 親クラスの静的メンバー変数と静的コードブロック
- 子クラスの静的メンバー変数と静的コードブロック
- 親クラスの通常メンバー変数とコードブロック、親クラスのコンストラクタ
- 子クラスの通常メンバー変数とコードブロック、子クラスのコンストラクタ
クラスのアクティブ参照とパッシブ参照#
Java 仮想マシンの仕様では、クラスに対するアクティブ参照のみが、その初期化メソッドをトリガーします。それ以外の参照方法はパッシブ参照と呼ばれ、クラスの初期化メソッドをトリガーしません。
アクティブ参照
アクティブ参照:クラスローディングフェーズでは、ロードとリンク操作のみが実行され、初期化操作は実行されません。
パッシブ参照
アクティブ参照以外の参照状況はすべてパッシブ参照と呼ばれ、これらの参照は初期化を行いません。
パッシブ参照のいくつかの形式:
- 子クラスが親クラスの静的フィールドを参照することで、子クラスの初期化は行われません。
- クラスの配列を定義して値を割り当てない場合、そのクラスの初期化はトリガーされません。
- クラスで定義された定数にアクセスすることで、そのクラスの初期化はトリガーされません。
三、三種類のクラスローダー#

- Bootstrap Classloader は Java 仮想マシンが起動した後に初期化されます。
- Bootstrap Classloader は ExtClassLoader をロードし、ExtClassLoader の親ローダーを Bootstrap Classloader に設定します。
- Bootstrap Classloader が ExtClassLoader をロードした後、AppClassLoader をロードし、AppClassLoader の親ローダーを ExtClassLoader に指定します。
Bootstrap ClassLoader#
起動クラスローダー:JDK\jre\lib(JDK は JDK のインストールディレクトリを指します)に保存されている、または - Xbootclasspath パラメータで指定されたパスにある、仮想マシンが認識できるクラスライブラリ(例えば rt.jar、すべての java. で始まるクラスは Bootstrap ClassLoader によってロードされます)。起動クラスローダーは Java プログラムから直接参照することはできません。
Extension ClassLoader#
拡張クラスローダー:このローダーは sun.misc.Launcher$ExtClassLoader によって実装されており、JDK\jre\lib\ext ディレクトリ内、または java.ext.dirs システム変数で指定されたパスにあるすべてのクラスライブラリ(例えば javax. で始まるクラス)をロードします。開発者は拡張クラスローダーを直接使用できます。
Application ClassLoader#
アプリケーションクラスローダー:このクラスローダーは sun.misc.Launcher$AppClassLoader によって実装されており、ユーザークラスパス(プログラム自身の classpath 内のクラス)で指定されたクラスをロードします。開発者はこのクラスローダーを直接使用でき、アプリケーション内で独自のクラスローダーを定義していない場合、通常はこれがプログラムのデフォルトのクラスローダーです。
クラスローダーの隔離問題#
各クラスローダーは、ロードされたクラスを保存するための独自の名前空間を持っています。クラスローダーがクラスをロードする際、名前空間に保存されたクラスの完全修飾名(Fully Qualified Class Name)を使用して、そのクラスがすでにロードされているかどうかを検出します。
JVM および Dalvik はクラスを一意に識別するためにClassLoader id + PackageName + ClassNameを使用します。そのため、実行中のプログラム内には、パッケージ名とクラス名が完全に一致する 2 つのクラスが存在する可能性があります。また、これらのクラスが異なる ClassLoader によってロードされている場合、あるクラスのインスタンスを別のクラスに強制的にキャストすることはできません。これが ClassLoader の隔離性です。
クラスローダーの隔離問題を解決するために、JVM は親委任メカニズムを導入しました。
四、親委任モデル#
核心思想:第一に、下から上にクラスがすでにロードされているかを確認する;第二に、上から下にクラスをロードしようとする。
親委任モデルの作業フローは次のとおりです:クラスローダーがクラスロードのリクエストを受け取った場合、最初に自分でそのクラスをロードしようとはせず、リクエストを親ローダーに委任して完了させます。すべてのクラスロードリクエストは最終的に最上位の起動クラスローダーに渡されるべきであり、親ローダーがその検索範囲内で必要なクラスを見つけられない場合、つまりそのロードを完了できない場合にのみ、子ローダーが自分でそのクラスをロードしようとします。
具体的なロードプロセス#
- AppClassLoader がクラスをロードする際、最初に自分でそのクラスをロードしようとはせず、クラスロードリクエストを親クラスローダー ExtClassLoader に委任します。
- ExtClassLoader がクラスをロードする際、最初に自分でそのクラスをロードしようとはせず、クラスロードリクエストを BootstrapClassLoader に委任します。
- BootstrapClassLoader がロードに失敗した場合(例えば、% JAVA_HOME%/jre/lib 内でそのクラスが見つからない場合)、ExtClassLoader を使用してロードを試みます。
- ExtClassLoader もロードに失敗した場合、AppClassLoader を使用してロードを試み、AppClassLoader も失敗した場合は ClassNotFoundException の例外が発生します。
親委任モデルの意義#
- システムクラスはメモリ内に同じバイトコードの複数のコピーが存在するのを防ぎ、クラスに階層的な分割をもたらします。
- Java プログラムが安全かつ安定して実行されることを保証します。
例えば、java.lang.Objectをロードする場合、最終的には Bootstrap ClassLoader によってロードされます。つまり、最終的には Bootstrap ClassLoader が <JAVA_HOME>\lib 内のrt.jarから java.lang.Object を JVM にロードします。このように、悪意のある者が独自に java.lang.Object を作成し、不正なコードを埋め込んだ場合でも、親委任モデルに従ってクラスロードを実装すれば、最終的に JVM にロードされるのは rt.jar 内のものだけであり、これらのコア基盤クラスコードが保護されます。
拡張
なぜ java spi が親委任モデルを破壊するのか?
五、クラスのロード方法#
- コマンドラインからアプリケーションを起動する際に JVM が初期化してロードします。
- Class.forName () メソッドを使用して動的にロードします。
- ClassLoader.loadClass () メソッドを使用して動的にロードします。
Class.forName () と ClassLoader.loadClass ()
- Class.forName ():クラスの.class ファイルを JVM にロードし、クラスを解釈する際にクラス内の static 静的コードブロックを実行します。
- ClassLoader.loadClass ():単に.class ファイルを JVM にロードするだけで、static コードブロック内の内容は実行されず、newInstance の際にのみ実行されます。
六、自作ローダー#
アプリケーションはこれら 3 種類のクラスローダーが相互に協力してロードされますが、必要に応じて自作のクラスローダーを追加することもできます。JVM に付属の ClassLoader は標準の Java クラスファイルをローカルファイルシステムからロードすることしかできないため、自作の ClassLoader を作成することで、以下のようなことが可能になります:
- 信頼できないコードを実行する前に、自動的にデジタル署名を検証します。
- ユーザーの特定のニーズに合ったカスタマイズされた構築クラスを動的に作成します。
- 特定の場所から Java クラスを取得します。例えば、データベースやネットワークから。