JAVAシリーズなどのローディングメカニズムのハードコア分析5Kワード

通常、Java のクラス ロード部分で上記の質問に遭遇します。この記事では、重要な基本概念やアプリケーション シナリオを含む、クラス ロードの重要な部分を詳細に紹介します。また、作成者が知識ポイントを再理解して強化するのにも役立ちます。読んだ後、読者の役に立つことを願っています。

1. クラスロード処理

プログラマが作成したJavaソースプログラム(.javaファイル)は、コンパイラによってコンパイルされた後、バイトコード(.classファイル)に変換され、クラスローダは、.classファイル内のバイナリデータをメモリ上に読み込んで、メソッド領域のクラスのデータ構造をカプセル化するために、ヒープ領域に java.lang.Class オブジェクトを作成します。

クラス ロードの最終生成物は、ヒープ領域に配置される Class オブジェクトです。Class オブジェクトは、メソッド領域のクラスのデータ構造をカプセル化し、Java プログラマにメソッド領域のデータ構造にアクセスするためのインターフェイスを提供します。

以下は、クラスのロードプロセスを示す例です。

2. クラスのライフサイクル

クラスのライフサイクルには、ロード、検証、準備、分析、初期化、使用、アンロードの 7 つの段階が含まれます。このうち、ロード、検証、準備、初期化、アンロードの 5 つの段階がこの順序で段階的に開始されますが、解析段階は必ずしも必要ではありません。場合によっては、初期化後に開始することもできます。 Java 言語のランタイム バインディング (動的バインディングまたは遅延バインディングとも呼ばれ、実際にはポリモーフィズムです)、親クラスのメソッドをオーバーライドするサブクラスなど。

注: ここで書かれているのはステップバイステップの開始であり、ステップバイステップの進行や完了ではありません。これらのステージは通常、インターリーブされて互いに混合されており、通常は 1 つのステージの実行中に別のステージを呼び出してアクティブ化するためです。 。

1.ロード

読み込みフェーズでは次の 3 つのことを行います。

  • このクラスを定義するバイナリ バイト ストリームを完全修飾名で取得します。

  • このバイト ストリームで表される静的ストレージ構造をメソッド領域の実行時データ構造に変換します。

  • このクラスを表す java.lang.Class オブジェクトは、メソッド領域のデータへのアクセス エントリとして Java ヒープに生成されます。

ここの最初の点では、どこでどのように入手するかが指定されていないため、開発者にとっては拡張の余地があります。次のような多くの Java テクノロジはこの基盤に基づいて構築されています。

  • JAR、WAR などの ZIP パッケージから読み取ります。

  • ネットワークから取得される、このシナリオの最も典型的なアプリケーション シナリオ アプリケーションはアプレットです。

  • ランタイム計算の生成で最も使用されるシナリオは、Spring AOP などの動的プロキシ テクノロジです。

ロードフェーズが完了すると、仮想マシンの外部にあるバイナリバイトストリームが仮想マシンが要求する形式に従ってメソッド領域に格納され、クラス java.lang.Class のオブジェクトも Java ヒープに作成されます。オブジェクトはメソッド領域でこのデータにアクセスします。

2. 検証

ロードされたクラスの正確性を確認するために、クラスは 4 つの検証段階に分かれています。

  • ファイル形式の検証

  • メタデータの検証

  • バイトコード検証

  • シンボリック参照の検証

検証フェーズは非常に重要ですが、必須ではありません。プログラムの実行時間には影響しません。参照されたクラスが繰り返し検証されている場合は、 -Xverifynone パラメータを使用してクラス検証手段のほとんどをオフにして、時間を短縮することを検討できます。仮想マシンクラスのロード時間。

3. 準備する

クラスの静的変数のメモリ確保とデフォルト値の初期化はメソッド領域に確保されるため、以下の点に注意が必要です。

  • ここで割り当てられる変数には、オブジェクトのインスタンス化とともに Java ヒープに割り当てられるインスタンス変数ではなく、クラス変数 (静的) のみが含まれます。

  • ここでのデフォルト値は、コードに示されている割り当てられた値ではなく、データ型のデフォルト値 (0、0L、null、false など) です。

  • ConstatntValue 属性がクラス フィールドのフィールド属性テーブルに存在する場合、つまり、final と static の両方によって変更される場合、変数値は準備段階で ConstValue 属性で指定された値に初期化されます。

4. 分析

解析フェーズは、仮想マシンが定数プール内のシンボリック参照を直接参照に置き換えるプロセスです。解析アクションは主に、クラスまたはインターフェイス、フィールド、クラス メソッド、インターフェイス メソッド、メソッド タイプ、メソッドハンドルとコールポイント修飾子。シンボル参照は、ターゲットを説明する一連のシンボルであり、任意のリテラルを使用できます。

直接参照は、ターゲットを直接指すポインタ、相対オフセット、またはターゲットを間接的に特定するハンドルです。

5. 初期化

クラスの静的変数に正しい初期値を割り当てると、JVM がクラスの初期化 (主にクラス変数の初期化) を担当します。初期化フェーズは、クラス コンストラクター メソッドを実行するプロセスです<client>()

  • <client>()このメソッドは、コンパイラによって自動的に収集されたクラス内のすべてのクラス変数代入アクションとstatic{}静的ステートメント ブロック内のステートメントの組み合わせによって生成されます。コンパイラによる収集の順序は、ステートメントがソース内に出現する順序によって決まります。ファイル。静的ステートメント ブロック内でアクセスできるのは、静的ステートメント ブロックよりも前に定義された変数のみです。静的ステートメント ブロックの後に定義された変数は、割り当てることはできますが、アクセスすることはできません。次のように:

  • <clinit>()このメソッドはクラス コンストラクターとは異なります。親クラス コンストラクターを明示的に呼び出す必要はありません。仮想マシンは、サブクラスのメソッドが実行される前に、親クラスのメソッドが実行されることを保証し<clinit>()ます<clinit>()

  • 親クラスのメソッドが<clinit>()先に実行されるため、親クラスの静的ステートメントブロックがサブクラスの変数代入操作よりも優先されることを意味し、以下に示すように、最終値は1ではなく2になります。

 
 

ジャワ

コードをコピーする

public class TestClassLoader { public static int A = 1; static { A = 2; // System.out.println(A); } static class Sub extends TestClassLoader { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); } }

  • <clinit>()クラスやインターフェイスにはメソッドは必要ありません。クラスに静的ステートメント ブロックや変数代入操作がない場合、<clinit>()メソッドは生成されません。

  • インターフェイスとクラスの違いは、インターフェイスは最初に親クラスの<clinit>()メソッドを実行する必要がなく、親インターフェイスによって定義された変数が使用される場合にのみ親インターフェイスが初期化されることです。また、インターフェースの実装クラスは、<clinit>()インターフェースのメソッドを最初に実行しません。

  • <clinit>()仮想マシンは、複数のスレッドがクラスを初期化する場合、1 つのスレッドのみがメソッドを実行し、他のスレッドはブロックされることを保証します。

    <clinit>()メソッドと<init>()メソッドの違い:

  • 実行タイミングが異なります。init メソッドはオブジェクト コンストラクター メソッドであり、オブジェクトが新規でオブジェクトのコンストラクター メソッドが呼び出されたときに実行されます。clinit メソッドは、JVM ロード時の初期化フェーズで呼び出されるクラス コンストラクター メソッドです。

  • 実行の目的は異なります。init は非静的変数を分析して初期化することですが、clinit は静的変数と静的コード ブロックを初期化することです。

3. 保護者の委任メカニズム

親委任メカニズムを紹介する前に、次のようなクラス ローダーの階層関係図を見てみましょう。

  • スタートアップ クラス ローダー (Bootstrap ClassLoader) は、$JAVA_HOME\jre\lib または -Xbootclasspath パラメーターで指定されたパスに格納され、仮想マシンによって認識されるクラス ライブラリ (rt.jar、すべての Java など) をロードする役割を果たします。 .* で始まるクラスは、Bootstrap ClassLoader によってロードされます)。スタートアップ クラス ローダーは、Java プログラムから直接参照することはできません。

  • 拡張クラスローダー (Extension ClassLoader)。ローダーは sun.misc.LauncherExtClassLoader によって実装され、ExtClassLoader 実装のロードを担当します。ExtClassLoader 実装のロードを担当します。JAVA_HOME\jre\lib\ext ディレクトリのロードを担当します。またはby java.ext .dirs システム変数で指定されたパス内のすべてのクラス ライブラリ (javax.* で始まるクラスなど) について、開発者は拡張クラス ローダーを直接使用できます。

  • Application ClassLoader (Application ClassLoader)、クラスローダーは sun.misc.Launcher$AppClassLoader によって実装され、ユーザークラスパス (ClassPath) で指定されたクラスをロードします。アプリケーションが持っている場合、開発者はこのクラスローダーを直接使用できます。独自のクラス ローダーはカスタマイズされていません。通常、これはプログラムのデフォルトのクラス ローダーです。

  • カスタム クラス ローダー (User ClassLoader)。必要に応じて、カスタム クラス ローダーを追加することもできます。JVM に付属する ClassLoader は、ローカル ファイル システムから標準の Java クラス ファイルをロードする方法しか知らないからです。

親委任メカニズムとは、クラス ローダーがクラス ロード要求を受信した場合、最初にクラスを単独でロードしようとするのではなく、要求を親ローダーに委任して完了し、順番に処理を進めることを意味します。要求のロード 最終的には、最上位の起動クラスローダーに渡される必要があります。親ローダーが検索範囲内で必要なクラスを見つけられない場合、つまりロードが完了できない場合にのみ、子ローダーは次のことを試みます。クラスを単独でロードします。

親委任メカニズムをより明確に理解するために、jdk1.8 ソース コードの java.lang.ClassLoader.loadClass() メソッドの実装を見てみましょう。

 
 

ジャワ

コードをコピーする

public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }

上記のコード コメントは非常に明確です。最初に findLoadedClass メソッドを呼び出して、このクラスがロードされているかどうかを確認し、ロードされていない場合は、親のloadClass メソッドを最下位レベルから最上位レベルに呼び出します。すべての ClassLoader がこのクラスをロードしていない場合は、findClass メソッドを呼び出してこのクラスを検索し、次に findClass メソッドを最上位レベルから呼び出して、最後に見つからない場合は ClassNotFoundException をスローします。この設計の目的は、セキュリティを確保し、システム クラスの偽造を防止することです。

理解しやすいように、ロードの概略図を以下に示します。

4 番目に、カスタム クラス ローダーの適用

カスタム クラス ローダーには通常、次の 4 つのアプリケーション シナリオがあります。

  • ソースコード暗号化によるソースコード漏洩の防止

  • 依存関係の競合を防ぐために分離ロードを使用して、ロードクラスを分離します。

  • クラスのロード方法を変更します。

  • 拡張ロードソース。

1. ソースコードの暗号化

ソース コード暗号化の本質は、バイトコード ファイルを操作することです。パッケージ化するときにクラスを暗号化し、クラス ファイルをロードする前にカスタム クラスローダーを通じてクラス ファイルを復号化し、標準のクラス ファイル規格に従ってロードすることで、クラス ファイルの通常のロードが完了します。したがって、暗号化された jar パッケージは、復号化メソッドを実装できるクラスローダーによってのみ通常どおりロードできます。

2. 分離されたロードクラス

私たちがよく遭遇する頭痛の種は、jar パッケージのバージョンの依存関係の競合で、コードを書くのに 5 分かかり、パッケージを調整するのに丸 1 日かかります。

栗を取ります:

このプロジェクトでは、2 つの jar パッケージ (A と B)、および C の v0.1 および v0.2 バージョンも導入されています。v2 バージョンのログの類似性により、v1 バージョンと比較してエラー メソッドが追加されます。パッケージ化するとき、Maven は 1 つだけ選択できます。 C のバージョン (v1 バージョンが選択されていると仮定)。実行時には、デフォルトでプロジェクトのすべてのクラスが同じクラス ローダーでロードされるため、依存する C のバージョンがいくつであっても、最終的に JVM にロードされる C のバージョンは 1 つだけです。B が Log.error にアクセスしようとすると、Log には error メソッドがまったくないことがわかり、例外 java.lang.NoSuchMethodError がスローされます。これは階級対立の典型的な事例です。

この問題を解決するためにクラス分離テクノロジーが使用されます。さまざまなモジュールの jar パッケージをさまざまなクラスローダーでロードできるようにします。

JVM は、非常にシンプルかつ効果的な方法を提供します。これを私はクラス ローディング コンダクション ルールと呼んでいます。JVM は、現在のクラスのクラス ローダーを選択して、そのクラスが参照するすべてのクラスをロードします。たとえば、2 つのクラス TestA と TestB を定義すると、TestA は TestB を参照します。カスタム クラス ローダーを使用して TestA をロードする限り、実行時に TestA が TestB を呼び出すと、JVM によって TestB も使用されます。ローダーがロードされます。同様に、TestA に関連付けられたすべての jar パッケージのクラスとその参照クラスがカスタム クラス ローダーによってロードされる限り、この限りです。この方法では、モジュールのメイン メソッド クラスを別のクラス ローダーでロードするだけで済みます。その後、各モジュールはメイン メソッド クラスのクラス ローダーでロードされるため、複数のモジュールを異なるクラス デバイスでロードできます。これは、OSGi と SofaArk がクラス分離を実現できる中心原理でもあります。

3. ホットローディング/ホットデプロイメント

アプリケーションの実行中に再起動せずにソフトウェアをアップグレードするには、ホット デプロイとホット ロードの 2 つの方法があります。

Java アプリケーションの場合、ホット デプロイメントはサーバーの実行中にプロジェクトを再デプロイすること、ホット ロードは実行時にクラスを再ロードしてアプリケーションをアップグレードすることです。

ホット ロードを要約すると、コンテナの起動時にバックグラウンド スレッドを開始し、クラス ファイルのタイムスタンプの変更を定期的に検出し、クラスのタイムスタンプが変更された場合はクラスを再ロードします。リフレクション機構と比較すると、リフレクションは実行時にクラス情報を取得し、動的呼び出しを通じてプログラムの動作を変更します。また、ホットローディングとは、実行時にリロードしてクラス情報を変更し、プログラムの動作を直接変更することです。

ホット デプロイメントの原理は似ていますが、アプリケーション全体を直接リロードします。この方法はメモリを解放します。これはホット ロードよりもクリーンで完全ですが、時間もかかります。

4. 拡張ロードソース

バイトコード ファイルは、データベース、Web、モバイル デバイス、さらには TV セットトップ ボックスからロードでき、ソース コード暗号化とともに使用できます。たとえば、一部のキー コードは、モバイル U ディスクを介して読み取って JVM にロードできます。

著者:Yili Programming
リンク:https ://juejin.cn/post/7255137260881477688
出典:レアアースナゲット
著作権は著者に帰属します。商業的転載の場合は著者に連絡して承認を求め、非商業的転載の場合は出典を明記してください。

おすすめ

転載: blog.csdn.net/wdj_yyds/article/details/131725749