JVM シリーズ - クラスローダーのメカニズム#
内容整理自:
一、簡述#
仮想マシンはクラスを記述するデータを class ファイルからメモリにロードし、データを検証、変換解析、初期化を行い、最終的に仮想マシンが直接使用できる Java 型を形成します。これが仮想マシンのクラスローディングメカニズムです。
二、クラスのロードプロセスとライフサイクル#
クラスローディングのプロセスは三つのステップ(五つのフェーズ)に分かれます:ロード
-> リンク(検証、準備、解析)
-> 初期化
ロード#
ロードのプロセスの説明:
- クラスの完全修飾名を使用して.class ファイルを特定し、そのバイナリバイトストリームを取得します。
- バイトストリームが表す静的ストレージ構造をメソッド領域の実行時データ構造に変換します。
- Java ヒープ内にこのクラスの java.lang.Class オブジェクトを生成し、メソッド領域内のこれらのデータへのアクセスエントリとして使用します。
リンク#
リンク:検証、準備、解析の三つのステップを含みます。
検証#
検証はリンクフェーズの第一ステップであり、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 つのクラスが存在する可能性があります。また、これらの 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 の際に実行されます。
六、自作ローダー#
アプリケーションはこれら三種類のクラスローダーが相互に協力してロードされます。必要に応じて、自作のクラスローダーを追加することもできます。JVM に付属の ClassLoader は標準の Java クラスファイルをローカルファイルシステムからロードすることしか理解していないため、独自の ClassLoader を作成することで以下の点を実現できます:
- 信頼できないコードを実行する前に、デジタル署名を自動的に検証します。
- ユーザーの特定のニーズに合ったカスタマイズされた構築クラスを動的に作成します。
- 特定の場所から Java クラスを取得します。例えば、データベースやネットワークから。