【JVM】-【徹底理解Java仮想マシン学習メモ】-第7章 仮想マシンクラスロードの仕組み

概要

仮想マシンのクラス ロード メカニズムとは、Java 仮想マシンがクラスを記述するデータを Class ファイルからメモリにロードし、データを検証し、変換、解析、初期化して、最終的に Java 型を形成することを指します。仮想マシンによって直接使用されるプロセス

クラスがロードされるとき

クラスが仮想マシンのメモリにロードされてからメモリからアンロードされるまで、そのライフ サイクル全体がLoading Loadingを実行しますロード) 验证(検証検証_ _ _検証)準備(準備準備_ _ _ _ _ _ _ _準備) 解析(解決解決_ _ _ _ _解決)初期(初期初期_初期)使用( _ _ _ _ _ _ _ _ __使用)、およびアンロード(アンロード アンロード降ろしには7つの段階があり、この
うち検証、準備、分析のを総称して「荷降ろし」と呼びます接続する(リンク中 リンク中リンク)
クラスのライフサイクル
ロード、検証、準備、初期化、アンロード5 つの段階の順序が決まっており、この順序で段階的に開始する必要があります

初期化する必要があります

仮想マシンの仕様では、クラスを直ちに初期化する必要があるのは次の 6 つの状況のみであると厳密に規定されているため、当然のことながら、その前にロード、検証、準備が開始されます。

  1. new、getstatic、putstatic、または invokestatic の 4 つのバイトコード命令が発生したとき、クラスが初期化されていない場合は、その初期化フェーズを最初にトリガーする必要があります。これら 4 つの命令を生成できる一般的なシナリオは次のとおりです: new キーワードがオブジェクトをインスタンス化する; Get読み取るまたは、クラスの静的フィールドを設定します(final による変更を除き、結果はコンパイル中に定数プールの静的フィールドに入れられます。これらはグローバル不変条件とみなされ、クラスとはほとんど関係ありません)。クラスのメソッドの場合
  2. class へのリフレクション呼び出しを行うとき、クラスが初期化されていない場合は、最初に初期化する必要があります。
  3. クラスを初期化するときに、その親クラスが初期化されていないことが判明した場合は、最初にその親クラスの初期化をトリガーする必要があります。
  4. JVM の起動時に、ユーザーは実行するメイン クラス(main() メソッドを含むクラス) を指定する必要があり、仮想マシンは最初にメイン クラスを初期化します。
  5. インターフェースが JDK 8 で導入されたデフォルトのメソッドを定義する場合、このインターフェースの実装クラスが初期化される場合は、その前にインターフェースを初期化する必要があります。

クラスは使用する必要があるため当然初期化する必要があるため、上記の動作はすべてクラスを使用する必要がある動作であり、これらの動作はクラスへのアクティブ参照
と呼ばれますさらに、型を参照するすべての方法は、クラスを参照しますが、クラスの初期化をトリガーしません (これはパッシブ参照と呼ばれます。例:サブクラスを介して親クラスの静的フィールドを参照する、サブクラスを参照する、サブクラスは初期化されますが、そうではありません。配列定義を介してクラスを参照しても、このクラスの初期化はトリガーされません。たとえば、ユーザー クラスの初期化はトリガーされません。定数(静的な最終変更) は定数に格納されます。コンパイル段階での呼び出しクラスのプール本質的に、定数を定義するクラスへの直接参照はないため、定数を定義するクラスの初期化はトリガーされません。User[] users = new User[10]

クラスロードプロセス

ロード、検証、準備、解析、初期化を含むクラスロードのプロセス全体

負荷

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

  1. クラスを完全修飾名で定義するバイナリ バイト ストリームを取得します
  2. このバイト ストリームで表される静的ストレージ構造をメソッド領域のランタイム データ構造に変換します。
  3. メソッド領域内のこのクラスのさまざまなデータへのアクセス ポイントとして、このクラスを表すjava.lang.Classオブジェクトをメモリ内に生成します。

確認する

検証フェーズの目的は、クラス ファイルのバイト ストリームに含まれる情報が「Java 仮想マシン仕様」のすべての制約に準拠していることを確認し、この情報が仮想マシン自体のセキュリティを危険にさらさないことを確認することです。コードとして実行した後、Class ファイルはコンパイルされないため、
Java ソース コードからのみコンパイルする必要があり、キーボード 0 と 1 を使用してバイナリ エディタ上で Class ファイルを直接入力するなどの方法で生成できます。JVM が入力バイト ストリームをチェックせず、それを完全に信頼している場合、エラーや悪意のあるバイト コード ストリームがロードされる可能性があり、システム全体が攻撃されたり、クラッシュする可能性があります。そのため、バイト コードの検証は必要です。 JVM 自身を保護するための手段です。この段階が厳格であるかどうかは、JVM が
悪意のあるコードからの攻撃に耐えられるかどうかを直接決定します。全体として、検証段階では、ファイル形式の検証、メタデータの検証、バイトコードの検証、シンボルの 4 つの検証アクションが大まかに完了します。参照検証

準備する

準備段階は次のように正式に定められています。クラス内で定義された変数(つまり、静的変数、静的によって変更された変数) メモリを割り当て、クラス変数の初期値を設定する段階JDK 7 より前では、HotSpot は永続生成を使用してメソッド領域を実装するため、クラス変数はメソッド領域に格納されます。JDK 8 以降では、クラス変数は Class オブジェクトとともに Java ヒープに格納されるため、「クラス変数はメソッド エリア。エリア」は完全な論理式です。
現時点では、メモリ割り当てにはクラス変数のみが含まれ、インスタンス変数は含まれません。インスタンス変数は、オブジェクトがインスタンス化されるときにオブジェクトとともに Java ヒープに割り当てられます。第二に、ここで言う「初期値」は「通常」データ型 のゼロ値です。たとえば、クラス変数が として定義されている場合private static int v = 123;、準備フェーズ後の変数 v の初期値は 123 ではなく 0 になります。まだ開始されていません Java メソッドを実行します。v を 123 に割り当てる putstatic 命令は、プログラムのコンパイル後にクラス コンストラクターの <clinit>() メソッドに格納されるため、v を 123 に割り当てるアクションは実行されません。初期化フェーズ。
「通常の状況」では初期値が 0 であることは上で述べました。クラス フィールドのフィールド属性テーブルにConstantValue属性が存在する場合、変数値は ConstantValue で指定された初期値に初期化されます。準備段階での属性、つまりクラス変数が静的な最終変更の場合は、指定された値が割り当てられます。たとえば、private static final int v = 123;準備段階では v は 123 に設定されます。

解析する

解析フェーズは、JVM が定数プール内のシンボル参照を直接参照に置き換えるプロセスです。

初期化

初期化フェーズはクラス ロード プロセスの最後のステップです。上記のいくつかのクラス ロード アクションのうち、カスタム クラス ローダーを介してロード フェーズに部分的に参加できるユーザー アプリケーションを除き、残りのアクションは完全に実行されます。 Java: 仮想マシンが制御を取得します。JVM が実際にクラスに記述された Java プログラム コードの実行を開始するのは、初期化フェーズになって初めて、アプリケーションに主導権が渡されます。準備フェーズでは、変数にはゼロ値が割り当てられ、初期化フェーズでは
、プログラマーがプログラムコーディングを通じて設定した値は、クラス変数やその他のリソースの初期化を計画します。より直接的なステートメントは次のとおりです。初期化フェーズは、クラス コンストラクターの <clinit>() メソッドを実行するプロセスです。

  1. <clinit>() メソッドは、クラス内のすべてのクラス変数割り当てアクション静的コード ブロック内のステートメントを自動的に収集するコンパイラ (javac.exe) によって生成されます。コンパイラのコレクションの順序は、ソース内のステートメントによって決まります。ファイルに表示される順序は、静的コード ブロックによって決まります。静的コード ブロックの前に定義された変数のみにアクセスでき、その後に定義された変数には、前の静的コード ブロックで値を割り当てることができますが、アクセスすることはできません。 (つまり、書き込みのみ可能ですが、読み取りはできません。静的コード ブロックの後に他の代入ステートメントが存在する可能性があるため、静的コード ブロックの実行時に読み取り値が変数の最終値ではない可能性があるため、読み取りは行われません。許可された)
  2. <clinit>() メソッドは、クラス コンストラクター (<init>() メソッド) とは異なります。親クラス コンストラクターを明示的に呼び出す必要はありません。JVM は、サブクラスの <clinit の前に親クラス コンストラクターが実行されるようにします。 >() メソッドが実行されました。 クラスの <clinit>() メソッドが実行されましたしたがって、JVM で実行される最初の <clinit>() メソッドのクラスは java.lang.Object である必要があります。
  3. <clinit>() メソッドは、クラスまたはインターフェイスには必要ありません。クラス内に静的コード ブロックも変数への代入ステートメントも存在しない場合、コンパイラは、クラスまたはインターフェイスに対して <clinit>() メソッドを生成する必要はありません。このクラス。
  4. 静的コード ブロックはインターフェイスでは使用できませんが、変数初期化のための代入操作が存在するため、<clinit>() メソッドも生成されます。ただし、クラスとは異なり、インターフェイスの <clinit>() メソッドを実行する場合、最初に親インターフェイスの <clinit>() メソッドを実行する必要はありません。これは、親インターフェイスは、親インターフェイスで定義されている変数が初期化された場合にのみ初期化されるためです。
    また、インターフェイスの実装クラスは、初期化中にインターフェイスの <clinit>() メソッドを実行しません。サブクラスが初期化されるときは、サブクラスが初期化される前に親クラスの初期化が行われる必要があります。
  5. JVM は、クラスの <clinit>() メソッドがマルチスレッド環境で正しくロックされ、同期されていることを確認する必要があります。複数のスレッドが同時にクラスを初期化した場合、スレッドのうちの 1 つだけが <clinit>(このクラスの ) メソッドを使用する場合、他のスレッドはブロックされ、アクティブなスレッドが <clinit>() メソッドの実行を完了するまで待機する必要があります。もちろん、アクティブなスレッドが <clinit>() メソッドの実行を終了した後、待機中の他のスレッドは目覚めた後に <clinit>() メソッドの実行を継続しません。同じクラス ローダーの下では、型は 1 回だけ初期化されます

クラスローダー

クラスローダーとは、クラスのロードフェーズで「クラスの完全修飾名を通じてクラスを記述するバイナリバイトストリームを取得する」アクションを実装するコードを指します。

クラスとクラスローダー

どのクラスでも、それをロードするクラス ローダークラス自体(つまり、完全修飾クラス名) は、Java 仮想マシン内でその一意性を共同で確立する必要があります。つまり、2 つのクラスが「等しい」かどうかの比較は、2 つのクラスが同じクラス ローダーによってロードされた場合にのみ意味を持ちます。そうでない場合は、2 つのクラスが同じ Class ファイルから生成され、同じ Java 仮想マシンのロードによってロードされ場合でも、それらをロードするクラスローダーが異なる限り、2 つのクラスは等価であってはなりません。

保護者の委任モデル

Java 仮想マシンの観点から見ると、異なるクラス ローダーは 2 つだけです。1
つはスタートアップ クラス ローダー ( Bootstrap C lass Loader Bootstrap\ Class\ Loaderブートストラップクラスローダー  (ブート クラス ローダー呼ばます)。このクラスローダーC++ 言語で実装されており、仮想マシン自体の一部です。このクラス ローダーインスタンス取得 (クラス ローダーの getParent() メソッドが null を返した場合、その親クラス ローダーがブート クラス ローダーであることを意味します)。

もう 1 つは他のすべてのクラス ローダーです。これらのクラス ローダーはすべて Java 言語で実装され、すべて仮想マシンの外部に存在し、すべて抽象クラス java.lang.ClassLoader から継承されます。まず、次によって提供されるクラス ローディングを理解しましょう。 3系統の
デバイス:

  • クラス ローダーを開始します。このクラス ローダーは、<JAVA_HOME>\lib ディレクトリに保存されているクラス (つまり、パッケージ名 java、javax、sun で始まるクラスを含む Java のコア クラス ライブラリをロードします)、または次のコマンドで指定されたクラスをロードします。 -Xbootclasspath パラメータパスに保存され、Java 仮想マシンによって認識されるクラス ライブラリは、仮想マシンのメモリにロードされます。スタートアップ クラス ローダーは Java プログラムから直接参照できません。ユーザーがカスタム クラス ローダーを作成するときに、処理のためにロード リクエストをブート クラス ローダーに委任する必要がある場合は、代わりに null を直接使用できます。つまり、null 値を使用できます。ブートクラスを表すクラスローダー
  • 拡張クラス ローダー(拡張クラス ローダー Extension\ Class\ Loader拡張クラスローダー) :このクラスローダー、クラスsun.misc.Launcher $ExtClassLoader 内Javaコードとして実装  ます<JAVA_HOME>\lib\ext ディレクトリ、または java.ext.dirs システム変数で指定されたパスにあるすべてのクラス ライブラリをロードします。これは実際には Java システムのクラス ライブラリの拡張メカニズムであり、JDK 開発チームはユーザーが汎用クラス ライブラリを ext ディレクトリに配置して Java SE の機能を拡張できるようにしています。拡張クラス ローダーは Java コードで実装されているため、開発者は拡張クラス ローダーをプログラム内で直接使用して、クラス ファイルをロードできます。
  • アプリケーション クラス ローダー(アプリケーション クラス ローダー Application\ Class\ Loaderアプリケーションクラスローダー) :このクラスローダーsun.misc.Launcher   $ AppClassLoaderによって実装ますClassLoader クラスの getSystemClassLoader() メソッドの戻り値であるため、「システム クラス ローダー」ユーザー クラス パス (ClassPath ClassPath )クラスパス、プロジェクト内の任意のクラスを呼び出すことで、 ClassPathxxx.class.getResource("/").toString()のすべてのクラス ライブラリを取得できます。開発者は、このクラス ローダーをコード内で直接使用することもできます。アプリケーションが独自のクラス ローダーをカスタマイズしていない場合、通常、これがプログラム内のデフォルトのクラス ローダーになります。

JDK 9 より前の Java アプリケーションは、これら 3 つのクラス ローダーの連携によってロードされます。ユーザーは、ディスクの場所以外のクラス ファイル ソースを追加するなど、拡張するカスタム クラス ローダーを追加することもできます。ローダーは、クラス分離やオーバーロードなどの機能を実装します。 、これらのクラスローダー間の協力関係は「通常」次のようになります (「通常」の理由は、JDK 1.2 の導入以降、親委任モデルが広く使用されていますが、これはバインディング モデルではなく、最適なモデルであるためです) Java 設計者が開発者に推奨するクラスローダー実装の練習):
保護者の委任モデル

クラス ローダー間のこの階層関係は、クラス ローダーの親委任モデル(親委任モデル、親\ 委任\ モデル)です。保護から委任モデル) _ _ _ _ _ _ _ _ _ _ _ _ _   _

親委任モデルでは、トップレベルの起動クラス ローダーに加えて、他のすべてのクラス ローダーにも独自の親クラス ローダーが必要です。ここで説明する親子関係は、通常、継承によって実装されるのではなく、組み合わせ関係を使用して親ローダーのコードを再利用することによって実装されます。

ワークフローは次のとおりです:クラス ローダーがクラス ロード リクエストを受信した場合、最初にクラス自体をロードしようとするのではなく、完了するためにリクエストを親クラス ローダーに委任します。クラス ローダーの各レベルは、そのとおりです。つまり、すべてのロード リクエストは最終的には最上位の起動クラス ローダーに送信される必要があります。親ローダーがロード リクエストを完了できない (つまり、必要なクラスが検索スコープ内に見つからない) とフィードバックされた場合にのみ、サブローダーはロードを完了しようとします。それ自体で。

このモデルでは、明らかな利点は、 Java のクラスには、どのクラス ローダーがこのクラスをロードしようとしているかに関係なく、 rt.jar に格納されている java.lang.Object などのクラス ローダーとともに優先順位との階層関係があることです。 , 最終的にはロードのためにモデルの最上位にあるスタートアップ クラス ローダーに委任されます。したがって、オブジェクトクラス、プログラムのさまざまなクラス ローダー環境で同じクラスであることが保証されます。

  1. クラスの繰り返しロードを回避します。親ローダーがすでにクラスをロードしている場合、子ローダーはクラスを再ロードしません。
  2. 安全性は保証されています。<JAVA_HOME>\lib ディレクトリ内のクラスは、起動クラス ローダーによってのみロードされます。誰かが悪意を持って <JAVA_HOME>\lib ディレクトリと同じ名前のクラス (java.lang.Integer など) を定義した場合を想像してください。親委任モデルがない場合、そのようなクラスは正常にロードされ、使用されることもありますが、コアの基本的な Java API は上書きされ、改ざんされます。

親委任モデルを実装するコードはすべて、java.lang.ClassLoader のloadClass() メソッドに集中しています。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    
    
    synchronized (getClassLoadingLock(name)) {
    
    
        //首先检查这个类是否被加载过
        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,说明父类加载器无法完成加载请求
            }
            if (c == null) {
    
    
                long t1 = System.nanoTime();
                //父类加载器无法加载时,再调用自身的findClass()方法来进行加载
                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;
    }
}

メソッドのロジックは次のとおりです: まず、ロードするように要求された型がロードされているかどうかを確認します。ロードされていない場合は、親クラス ローダーのloadClass() メソッドを呼び出します。親クラス ローダーが空の場合は、起動クラス ローダーが次のように使用されます。デフォルトでは親ローダー。親クラス ローダーがロードに失敗し、ClassNotFoundException をスローした場合は、独自の findClass() メソッドを呼び出してロードを試みます。

親の委任モデルを打破する

上記の内容からわかるように、親委任モデルの実装は ClassLoader のloadClass() メソッド内にあるため、親委任モデルを破棄するには、クラス ローダーを自分で定義し、loadClass() メソッドをオーバーライドするだけで済みます

歴史上3度、親の委任モデルが崩壊した

  1. 初めての出来事は、実際には親の委任モデルが登場する前に起こりました。親委任モデルは JDK 1.2 でのみ利用可能であったため、クラス ローダーと抽象クラス java.lang.ClassLoader の概念は Java の最初のバージョンにすでに存在していました。) メソッド。既存のコードとの互換性を保つために、loadClass() メソッドがサブクラスによって上書きされる可能性を避けることはできません。新しい findClass() メソッドを追加して、ユーザーが独自のメソッドを作成するときに可能な限りこのメソッドを書き直すようにすることしかできません。クラスロードロジック。loadClass()メソッドではありません。loadClass() メソッドの以前の分析から、親クラスがクラスのロードに失敗した場合、サブクラスは独自の findClass() メソッドを呼び出してロードを完了します。これは、ユーザーがクラスをロードするのに影響を与えないことがわかります。彼自身のロジックに従って、新しく作成されたクラスローダーが親委任モデルに準拠していることを確認します。
  2. 2 回目はモデル自体の欠陥が原因でした。親委任モデルが実現されました。より基本的なクラスは、上位レベルのローダーによってロードされます。ユーザーのコード (ClassPath の下) にコールバックする必要がある基本型がある場合、起動クラス ローダーはロードできません。たとえば、JDBC は Java 自体によって定義された一連の仕様であり、Java では非常に基本的な型である必要がありますが、他のメーカーによって実装され、アプリケーションの ClassPath の下にデプロイされた SPI (Service Provider Interface) コード、つまり MySQL を呼び出す必要があります。 、Oracle およびその他の企業 JDBC によって実装されたドライバー クラスに従って、JDBC をロードするクラス ローダーはこれらのコードを認識できません。
    この問題を解決するために、Java では、JDBC と同様のサービスをロードするために使用されるスレッド コンテキスト クラス ローダーが導入されています。必須の SPI サービス コード。これは、親クラス ローダーが子クラス ローダーにクラス ロードの完了を要求する動作であり、親の委任モデルに違反します。
  3. 3 回目は、コード ホット リプレースメントやモジュール ホット デプロイメントなどのホット デプロイメント テクノロジの追求によって引き起こされました。一部の実稼働システムでは、再起動せずにホット デプロイメントと更新を実装することが非常に重要です。関連する実装には、IBM の OSGi が含まれます。モジュラー ホット デプロイメントを実現するための鍵は、そのカスタム クラス ローダー メカニズムの実装です。多くのクラス検索操作はフラット クラス ローダーで実行され、これにより、親委任モデルのルールを学ぶことができます。

Tomcat は親委任モデルを破壊します

関連リンク

おすすめ

転載: blog.csdn.net/Pacifica_/article/details/123647893