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 プログラムから直接参照することはできません。
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 つのクラスが存在する可能性があります。また、これらの 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 クラスを取得します(例えば、データベースやネットワークから)。