[Sticky JVM] 5年ぶりにJVMのロードメカニズムを知る時が来ました!

[Sticky JVM] 5年ぶりにJVMのロードメカニズムを知る時が来ました!

  [[384021]]

この記事はWeChatの公開アカウント「Mu Xiaoma Nong」から転載したもので、著者はMu Xiaonongです。この記事を転載する場合は、穆小馬農の公式アカウントまでご連絡ください。

クラスの読み込み

Java 仮想マシンのクラス読み込みプロセスは、クラス ファイルをメモリに読み込み、クラス ファイル内のデータを検証、変換、解析、初期化し、最終的に仮想マシンで直接使用できる Java 型を形成することです。

コンパイル時に接続作業が必要な言語とは異なり、Java では、型の読み込み、接続、初期化がすべてプログラム実行中に完了します。この戦略により、クラスをロードする際のパフォーマンスのオーバーヘッドがわずかに増加しますが、Java アプリケーションの柔軟性が向上します。

クラスを使用するときに、クラスがディスクからメモリにロードされていない場合、JVM は 3 段階の戦略 (ロード、接続、初期化) を通じてクラスを初期化します。これら 3 つのステップを完了する JVM の名前は、クラス ロードまたはクラス初期化と呼ばれます。

クラスの読み込みタイミング

Java 仮想マシン仕様では、クラス ロードの最初の段階であるロードをいつ開始するかについて、必須の制約は課されていません。代わりに、仮想マシンの特定の実装によって決定されます。ただし、初期化段階については、仮想マシン仕様では、クラスを直ちに初期化する必要がある状況は「5 つだけ」であると厳密に規定されています (当然、その前に読み込み、検証、準備を開始する必要があります)。具体的な状況は以下のとおりです。

クラス ファイルをロードするタイミング:

シリアルナンバーコンテンツ
1 4つのバイトコード命令new、getstatic、putstatic、invokestaticに遭遇する
2 java.lang.reflectパッケージのメソッドを使用してクラスを反映する場合
3 クラスを初期化する場合、親クラスは初期化されないので、まず親クラスを初期化します。
4 仮想マシンが起動すると、ユーザーはメインクラス(main()を含むクラス)を指定します。
5 JDK1.7の動的言語サポートを使用する場合、java.lang.invoke.MethodHandleインスタンスが最終的にメソッドハンドルREF getStatic、REF putStatic、REF_invokeStaticを解決し、このメソッドハンドルに対応するクラスが初期化されていない場合

シーケンス番号1の詳細な説明:

  1. newキーワードを使用してオブジェクトをインスタンス化すると
  2. クラスの静的変数を読み取るとき(finalによって変更され、コンパイル時に結果が定数プールに配置される静的フィールドを除く)
  3. クラスの静的変数を設定する場合
  4. クラスの静的メソッドを呼び出すとき

注: newarray 命令は配列型自体の初期化のみをトリガーし、関連する型の初期化はトリガーしません。たとえば、newString[] は String[] クラスの初期化を直接トリガーするだけです。つまり、クラス [Ljava.lang.String の初期化をトリガーしますが、String クラスの初期化を直接トリガーすることはありません。

これら 4 つの命令を生成する最も一般的な Java コード シナリオは次のとおりです。

クラスの初期化をトリガーするこれらの 5 つのシナリオでは、仮想マシン仕様では非常に強力な修飾子「if and only if」が使用され、これらの 5 つのシナリオでの動作はクラスへのアクティブ参照と呼ばれます。さらに、クラスを参照する他のすべての方法では初期化がトリガーされません。これはパッシブ参照と呼ばれます。

クラスのインスタンス化とクラスの初期化は完全に異なる概念であることに注意してください。

  • クラスのインスタンス化とは、クラスのインスタンス (オブジェクト) を作成するプロセスを指します。
  • クラスの初期化とは、クラスの各メンバーに初期値を割り当てるプロセスを指し、クラスのライフサイクルの段階です。

受動的な引用の 3 つのシナリオ:

サブクラスを介して親クラスの静的フィールドを参照しても、サブクラスは初期化されません。

  1. /**
  2. * @プログラム: jvm
  3. * @クラス名 テスト1
  4. * @Description: サブクラスを介して親クラスの静的フィールドを参照しても、サブクラスは初期化されません。
  5. * @著者: 穆小農
  6. * @作成日: 2021-02-27 11:42
  7. * @バージョン 1.0
  8. **/
  9. パブリッククラスTest1{
  10.  
  11. 静的{
  12. システム。 out .println( "スーパークラスを初期化します!!!" );
  13. }
  14.  
  15. 公共 静的void main(String[] args) {
  16. int x = Son.count ;
  17. }
  18.  
  19. }
  20.  
  21. クラスFatherはTest1を拡張します{
  22. 静的 整数 カウント= 1;
  23. 静的{
  24. システム。 out .println( "Init father!!!" );
  25. }
  26. }
  27.  
  28. クラスSonはFatherを継承します{
  29. 静的{
  30. システム。 out .println( "Init son!!!" );
  31. }
  32. }

出力:

  1. スーパークラスを初期化します!!!
  2. イニ父さん!!!

静的フィールドの場合、このフィールドを直接定義するクラスのみが初期化されます。したがって、親クラスで定義された静的フィールドをサブクラスを通じて参照すると、サブクラスではなく親クラスの初期化のみがトリガーされます。サブクラスのロードと検証をトリガーするかどうかについては、仮想マシン内で明確に指定されておらず、仮想マシンの具体的な実装に依存します。 Sun HotSpot 仮想マシンの場合、-XX:+TraceClassLoading パラメータを使用して、この操作によってサブクラスがロードされることを確認できます。

上記の場合、count フィールドは Father クラスで定義されているため、クラスが初期化されます。さらに、Father クラスを初期化するときに、仮想マシンはその親クラス Test1 が初期化されていないことを検出するため、仮想マシンは最初に親クラス Test1 を初期化し、次にサブクラス Father を初期化しますが、Son は初期化されません。

配列定義を通じてクラスを参照しても、このクラスの初期化はトリガーされません。

  1. /**
  2. * @プログラム: jvm
  3. * @クラス名 テスト2
  4. * @説明:
  5. * @著者: muxiaonong
  6. * @作成日: 2021-02-27 12:03
  7. * @バージョン 1.0
  8. **/
  9. パブリッククラスTest2 {
  10.  
  11. 公共 静的void main(String[] args) {
  12. M[] m = 新しいM[8];
  13. }
  14.  
  15. }
  16.  
  17. クラスM{
  18. 静的{
  19. システム。 .println ( "Init M!!!" );
  20. }
  21. }

実行後、「Init M!!!」という出力がないことがわかります。これは、クラスの初期化フェーズがトリガーされていないことを示しています。

定数は、コンパイル フェーズ中に呼び出し元のクラスの定数プールに格納されます。本質的には、定数を定義するクラスを直接参照しないため、定数を定義するクラスの初期化はトリガーされません。

  1. /**
  2. * @プログラム: jvm
  3. * @クラス名 テスト3
  4. * @説明:
  5. * @著者: muxiaonong
  6. * @作成日: 2021-02-27 12:05
  7. * @バージョン 1.0
  8. **/
  9. パブリッククラスTest3 {
  10.  
  11. 公共 静的void main(String[] args) {
  12. システム。 .println(ConstClass.COUNT )出力します
  13. }
  14.  
  15. }
  16.  
  17. クラスConstClass{
  18. 静的最終整数 カウント= 1;
  19. 静的{
  20. システム。 .println( "ConstClass を初期化します!!!" ) ;
  21. }
  22. }

上記のコードを実行すると、InitConstClass の出力はありません。これは、ConstClass クラスの定数 COUNT が Java ソース コードで参照されているにもかかわらず、コンパイル フェーズでの定数伝播の最適化によって、定数値 "1" が Test3 定数プールに格納されているためです。定数 ConstClass.COUNT への参照は、実際には Test3 クラス独自の定数プールへの参照に変換されます。つまり、Test3 クラス ファイルには ConstClass クラスのシンボリック参照エントリが実際には存在せず、クラス ファイルにコンパイルされた後、2 つのクラスには関係がありません。

クラスのロードプロセス

Class というファイルがあり、ハードディスク上に静かに存在し、快適な生活を楽しんでいます。ハードディスクからメモリに到達するには、どのようなプロセスを経る必要がありますか?クラスがメモリに入るには 3 つのステップがあります。

  • 読み込み中
  • リンク
  • 初期化中

1. ロード

ロードはクラス ロード プロセスの段階です。ロードとは、現在のクラスのクラス ファイルをメモリに読み込み、java.lang.Class のオブジェクトを作成することを指します。つまり、プログラム内で任意のクラスが使用されると、システムは java.lang.Class オブジェクトを作成します。

読み込みフェーズでは、仮想マシンは次の 3 つの処理を完了する必要があります。

完全修飾名を使用してクラスを定義するバイナリ バイト ストリームを取得します (クラス ファイルからのみ取得できるとは指定されておらず、ネットワーク、動的生成、データベースなどの他のチャネルからも取得できます)。

このバイトストリームによって表される静的ストレージ構造をメソッド領域のランタイムデータ構造に変換します。

メソッド領域内のこのクラスのさまざまなデータへのアクセスポイントとして、このクラスを表す java.lang.Class オブジェクトをメモリ内に生成します。

通常、クラス ローダーは、クラスをロードするために、クラスが「最初に使用される」まで待つ必要はありません。 Java 仮想マシン仕様では、システムが特定のクラスをプリロードできます。ロードフェーズと接続フェーズの内容の一部は交互に実行されます。読み込みフェーズはまだ完了しておらず、接続フェーズが開始している可能性がありますが、フェーズ間で実行されるアクションは依然として接続フェーズの内容に属し、これら 2 つのフェーズの開始時間は依然として固定された順序を維持します。

2. 接続する

クラスがロードされると、システムは対応するクラス オブジェクトを生成し、リンク フェーズに入ります。このフェーズでは、クラスのバイナリ データを JRE にマージします。リンクフェーズは 3 つのサブフェーズに分かれています。

1.1 検証

検証は接続フェーズの最初のステップです。このフェーズの主な目的は、クラス ファイルのバイト ストリームに含まれる情報が現在の仮想マシンの要件を満たし、仮想マシン自体のセキュリティを危険にさらさないことを確認することです。 Java は C/C++ に比べて比較的安全な言語です。検証フェーズは非常に重要です。このフェーズが厳密であるかどうかによって、Java 仮想マシンが悪意のあるコードからの攻撃に耐えられるかどうかが決まります。検証入力のバイト ストリームがクラス ファイル形式の制約に準拠していない場合、仮想マシンは java.lang.VerifyError 例外またはサブクラス例外をスローします。一般的に、検証は、ファイル形式の検証、メタデータの検証、バイトコードの検証、シンボル参照の検証という 4 つの検証アクションに分けられます。

ファイル形式の検証: 主に、バイト ストリームがクラス ファイル形式の仕様に準拠し、現在のバージョンの仮想マシンで処理できるかどうかを検証します。主に以下の側面が含まれます。

  • ファイル形式はCAFEBABEで始まっていますか?
  • メジャーバージョンとマイナーバージョンが仮想マシンが処理できる範囲内にあるかどうか
  • 定数プールにサポートされていない定数タイプはありますか?
  • 定数を指す各種インデックス値は、存在しない定数や型に適合しない定数を指していますか?
  • CONSTANTUtf8info 型の定数に UTF8 エンコードに準拠していないデータが含まれているかどうか
  • クラスファイルのさまざまな部分とファイル自体が削除されているか、添付ファイルがあるかどうかに関する情報

メタデータ検証: 主にバイトコードで記述された情報の意味解析を実行します。主な目的は、クラスのメタデータに対してセマンティック検証を実行し、Java 言語の構文仕様に準拠しているかどうかを分析し、Java 言語仕様に準拠していないメタデータ情報がないことを確認することです。この段階の主な検証の側面は次のとおりです。

  • このクラスには親クラスがありますか (java.lang.Object を除く)
  • このクラスの親クラスが継承が許可されていないクラス(final によって変更されたクラス)を継承しているかどうか
  • このクラスが抽象クラスでない場合、親クラスまたはインターフェースに必要なすべてのメソッドを実装していますか?
  • クラス内のフィールドとメソッドは親クラスと競合しますか?

バイトコード検証: データフローと制御フローを通じてプログラムのセマンティクスが合法かつ論理的であるかどうかを分析する、最も重要かつ複雑な検証手順です。主にクラスのメソッド本体の検証分析を実行し、検証されたクラスが動作中に仮想マシンのセキュリティを危険にさらさないことを確認します。

オペランドスタックのデータ型と命令コードシーケンスがいつでも連携できることを保証する(例えば、オペランドスタックにint型のデータがある場合、それが使用されるときにlong型としてローカル変数テーブルにロードされないことが保証される)

ジャンプ命令は、メソッド本体の外側のバイトコード命令には追加されません。

メソッド本体のデータ変換が有効であることを確認します。たとえば、サブクラス オブジェクトを親クラスのデータ型に割り当てることはできますが、親クラスをサブクラスのデータ型に割り当てることはできません。

シンボル参照の検証: シンボル参照を直接参照に変換する場合、この変換は第 3 フェーズ (バイトコード検証) の解析フェーズ中に行われます。主な目的は、参照がアクセスされ、アクセスできないクラスの問題が発生しないようにすることです。

1.2 準備

クラス変数にメモリを割り当て、クラス変数の初期値を設定する段階。これらの変数によって使用されるメモリはメソッド領域に割り当てられます。準備段階では、クラス ファイルの静的変数にデフォルト値が割り当てられます。注意: 初期値ではありません。たとえば、public static ini = 8 の場合、このステップでは i は 8 に割り当てられるのではなく、最初に 0 に割り当てられます。

基本タイプのデフォルト値:

データ型デフォルト値
整数 0
長さ 0L
短い (短い)0
文字 '\u0000'
バイト (バイト)0
ブール値間違い
フロート 0.0f
ダブル 0.0日
参照ヌル

通常、初期値は 0 ですが、上記の定数に final クラス修飾子を追加すると、初期値は指定した値になります。コンパイル時に、Javac は i の初期値を 8 に変更します。

1.3 分析

クラス ファイル定数プールで使用されるシンボリック参照を直接メモリ アドレスに変換し、その内容に直接アクセスします。シンボリック参照: 参照先ターゲットを説明するためにシンボルのセットを使用します。シンボルはリテラル形式の任意のリテラルにすることができます。競合がなく、位置を特定できる限り、直接参照できます。ターゲットを直接指すポインター、相対オフセット、または間接的にターゲットを特定できるハンドルにすることができます。直接参照がある場合、参照先ターゲットはメモリ内にすでに存在している必要があります。

3. 初期化

初期化とは、クラスの静的変数に正しい初期値を割り当てることです。先ほど、準備フェーズではデフォルト値をコピーし、初期化フェーズでは初期値を静的変数に割り当てることを説明しました。次の文を見てください。

  1. 公共 静的 整数i = 8

まず、バイトコード ファイルがメモリにロードされた後、接続検証が最初に実行されます。準備段階を経て、i にメモリが割り当てられます。静的であるため、この時点でのiのデフォルトの初期値はint型の0に等しいため、iは0になります。初期化になると、iには実際には8の値が割り当てられます。

クラスローダー

クラス ローダーは、すべてのクラスをロードし、メモリにロードされたクラスの java.lang.Class インスタンス オブジェクトを生成する役割を担います。クラスが JVM にロードされると、同じクラスは再度ロードされません。オブジェクトに一意の識別子があるのと同様に、JVM にロードされるクラスにも一意の識別子があります。 JVM 自体にはクラスローダー階層があります。このクラスローダー自体は通常のクラスです。すべてのクラスはクラス ローダーによってメモリにロードされます。これを ClassLoader、トップレベルの親クラス、抽象クラスと呼ぶこともできます。

ブートストラップ: クラス ローダーの読み込みプロセスは、読み込みのためにさまざまなレベルに分割されます。異なるクラスローダーは異なるクラスをロードします。トップレベルの Bootstrap として、rt.jar、charset.jar、その他のコア クラスなど、JDK のコア コンテンツを lib にロードします。このローダーを取得するために getClassLoader() を呼び出し、結果が Null の場合、トップレベルのローダーに到達したことを意味します。

拡張機能: 拡張機能ローダー拡張クラス。拡張機能パッケージ内のさまざまなファイルをロードします。これらの拡張機能パッケージは、JDK インストール ディレクトリ jre/lib/ext の下の jar にあります。

アプリ: これは通常使用するアプリケーションで、クラスパスで指定されたコンテンツを読み込むために使用されます。

カスタム ClassLoader: カスタム ClassLoader は、独自のカスタム ローダーをロードします。 Custom ClassLoader の親クラスローダーはアプリケーションです。 Extension の親クラス ローダーは Bootstrap です。

注: これらは継承関係ではなく、委任関係です。

  1. パブリッククラスClassLoaderTest {
  2. 公共 静的void main(String[] args) {
  3. // 誰がメモリにロードしたかを確認します Bootstrap は C++ で実装されているため、結果はnullになります。
  4. // Javaにはこれに対応するクラスはありません
  5. システム。出力.println(String.class.getClassLoader());
  6.  
  7. //これは、コア クラス ライブラリのパッケージ内のクラスの実行です。クラスはBootstrapによってもロードされるため、実行結果はNullになります。
  8. システム。出力.println(sun.awt.HKSCS.class.getClassLoader());
  9.  
  10. //このクラスは ext ディレクトリの jar ファイルにあります。これを呼び出すと、結果は sun.misc.Launcher$ExtClassLoader@a09ee92 になります。
  11. システム。出力.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
  12.  
  13. // これは私たちが独自に作成した ClassLoad ローダーで、sun.misc.Launcher$AppClassLoader@18b4aac2 によってロードされます。
  14. システム。出力.println(ClassLoaderTest.class.getClassLoader());
  15.  
  16. // Exe の ClassLoader です。 getclass() を呼び出します。それはクラスでもあります。 getClassLoader を呼び出します。その ClassLoader の ClassLoader は Bootstrap なので、結果はNull になります。  
  17. システム。出力.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
  18.          
  19. }
  20. }

クラスローダーの継承

この図は、ClassLoader が文法的に誰を継承するかを示しています。この図は継承関係ではなく、単純な文法関係を示しています。上記のクラスの読み込みとは何の関係もないことを思い出してください。実際にはこの図は無視できます。

親の委任

親ローダー: 親ローダーは、「クラスローダーのローダー」でも「クラスローダーの親クラスローダー」でもありません。親委任は、子から親へ、そして親から子への親委任プロセスです。

クラス ローダーは、クラスのロード要求を受信すると、まずカスタムのクラスからそれを探します。同時に、内部的にキャッシュも維持します。キャッシュ内に見つかった場合は、結果が直接返されます。見つからない場合は、親クラスに委任し、親クラスは最上位の親クラスまでキャッシュ内を検索します。この時点でキャッシュから必要な結果が得られない場合、父親は「私はあなたのためにこれを行うことはできません。自分で行う必要があります」と言います。その後、息子は対応するクラスを照会し、自分でロードします。末っ子がまだ対応するクラスを見つけられない場合は、例外 Class Not Found Exception がスローされます。

なぜ親の委任なのか?

これはクラスローダーにとって必ず聞かれる面接の質問です。

主に安全のため、任意のクラスをメモリにロードできる場合、java.lang.String を記述して危険なコードを記述すると、セキュリティ上の問題が発生するでしょうか?また、Java コア API で定義された型が勝手に置き換えられないようにし、API ライブラリが勝手に変更されるのを防ぐことができます。 2つ目は効率の問題です。キャッシュがある場合は、キャッシュから直接取得します。親クラスまたは子クラスを何度も走査してクエリする必要はありません。

<<:  Zookeeperが分散ロックを実装する原理

>>:  エッジコンピューティング クラウドネイティブ オープンソース ソリューションの比較

推薦する

ウェブサイトの降格理由の分析

多くの人のウェブサイトのコレクションが突然減少し、スナップショットが後退しました。これは、ウェブサイ...

百度のインターネットエコシステムプロジェクト構築に向けた共同の取り組みについての考察

みなさんこんにちは。梁磊です。百度ウェブマスタープラットフォームで「手を携えてインターネットエコシス...

分散型 MySQL Binlog ストレージ システムのアーキテクチャ設計

1. キングバスの紹介1.1 Kingbusとは何ですか? kingbus は、raft の強力なコ...

ラオ・ミ・ノン氏が「ドメイン名投資」の経験を語る

オンラインでの収入というと、多くのウェブマスターがタオバオアフィリエイト、SEO最適化、注文受付など...

フレンドリーリンクを購入する際に遭遇するNつの状況について話す

タイトルを見ると、次のような疑問を抱かずにはいられない人もいるだろう。「Baidu Green Ra...

どのような Web ページのリダイレクトが不正行為とみなされないのでしょうか?

絶対的に正しいとか、絶対的に間違っているとかいうものは存在せず、この言葉は決して時代遅れになることは...

格安WindowsVPS-$30/4Gメモリ/160gSSD/4Tトラフィック/4コンピュータルーム/Windows03/08/12

cheapwindowsvpsは、数日前のメールで、特別版のベアメタル専用サーバーホスティング(サー...

優れた SEO にはどのような資質が必要ですか?

SEO には多くのことが関係しますが、始めるのは非常に簡単です。インターネットとコンピューターの操作...

ウェブサイトのプロモーションに本当に必要なものがわからない

ビジネスの世界は戦場のようなもので、ウェブサイトの構築は戦争を戦うようなものです。ウェブサイトの構築...

ビッグデータ環境におけるニッチなウェブサイト向けのインターネットマーケティング手法の例

少し前にA5で「ビッグデータ環境における起業のチャンス」という記事を見たのを覚えています。私は主流の...

史上最も高価なドメイン名トップ15、seo.comもそのリストに

ウェブサイトを作成することは難しくありませんが、ウェブページやビジネスに適したドメイン名を見つけるの...

トップ SEO 戦略 - Google の 1 ページ目に目立つ

ジョン・ログネルド更新日: 2007 年 5 月 11 日午後 12:00 (東部標準時)翻訳:方林...

どのようなウェブサイトがBaiduのホームページにランクインできるのか

みなさん、こんにちは。私はハルビン仮想現実ウェブサイト設計です。最近、百度のキーワードランキングが1...

Google、メールデータを検索結果に統合するテストを実施

Google がメールデータを検索結果に統合するテストを実施 (写真提供: Tencent Tech...

SEO の効果に影響を与える非技術的な要因は何ですか?

多くの SEO 担当者は、検索エンジンのダイナミクス、SEO テクニックの学習、ウェブサイトの最適化...