English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Javaクラスのロード全体の深い理解

Javaクラスのロード全プロセス

1つのJavaファイルがロードされてからアンロードされるまでのライフサイクルでは、全体で以下の4の段階:

>ロード->リンク(検証+>準備+>解析)->初期化(使用前の準備)->使用->アンロード

中でロード(カスタムロードを除く)+リンクプロセスは完全にJVMが担当しています。クラスに対して初期化作業を行うべき時(ロード+リンクはその前に完了しているため、JVMは厳格な規定(以下の4つの状況)があります:

1.new、getstatic、putstatic、invokestaticなどの4クラスがまだ初期化されていない場合、その直後に初期化作業を行います。それは実際には3以下の状況:クラスをnewでインスタンス化する場合、クラスの静的フィールドを読み取るまたは設定する場合(final修飾子で修飾された静的フィールドを除き、彼らは既に定数プールに格納されています)、および静的メソッドを実行する場合。

2.java.lang.reflectを使用して。*.クラスを反射的に呼び出す方法がクラスにまだ初期化されていない場合、すぐにそれを初期化します。

3.クラスを初期化する際に、その親がまだ初期化されていない場合、まずその親を初期化します。

4.JVMが起動するとき、ユーザーは実行する必要がある主クラス(static void main(String[] args)を持つクラス)を指定する必要があります。その場合、JVMはまずそのクラスを初期化します。

以上4このような前処理は、クラスに対する積極的な参照と呼ばれ、その他のすべての状況(以下にいくつかの被动参照の例を挙げます)は、クラスの初期化を引き起こしません。

/** 
 * 被動参照シチュエーション1 
 * 子クラスが親クラスの静的フィールドを参照する場合、子クラスの初期化を引き起こしません 
 * @author volador 
 * 
 */ 
class SuperClass{ 
  static{ 
    System.out.println("super class init."); 
  } 
  public static int value=123; 
} 
class SubClass extends SuperClass{ 
  static{ 
    System.out.println("sub class init."); 
  } 
} 
public class test{ 
  public static void main(String[]args){ 
    System.out.println(SubClass.value); 
  } 
} 

出力結果は:super class init。

/** 
 * 被動参照シチュエーション2 
 * クラスを参照するために配列を通じて参照することで、そのクラスの初期化を引き起こしません 
 * @author volador 
 * 
 */ 
public class test{ 
  public static void main(String[] args){ 
    SuperClass s_list=new SuperClass[10]; 
  } 
} 

出力結果:出力なし

/** 
 * 被動参照シチュエーション3 
 * 定数はコンパイル段階で呼び出しクラスの定数プールに格納されます。本質的には定数を定義するクラスに参照されていませんので、自然に定数を定義するクラスの初期化は発生しません 
 * @author root 
 * 
 */ 
class ConstClass{ 
  static{ 
    System.out.println("ConstClass init."); 
  } 
  public final static String value="hello"; 
} 
public class test{ 
  public static void main(String[] args){ 
    System.out.println(ConstClass.value); 
  } 
} 

出力結果:hello(ヒント:コンパイル時に、ConstClass.valueはhello定数としてtestクラスの定数プールに格納される)

上記はクラスの初期化に関するものであり、インターフェースも初期化する必要があります。インターフェースの初期化はクラスの初期化と少し異なります:

上記のコードはstatic{}を使用して初期化情報を出力していますが、インターフェースでは実現できません。しかし、インターフェースの初期化時、コンパイラはインターフェースのメンバー変数を初期化するために<clinit>()のクラスコンストラクタを生成します。この点はクラスの初期化でも同様です。実際に異なる点は、クラスの初期化は親クラスがすべて初期化された後に実行される必要があるが、インターフェースの初期化は親インターフェースの初期化に対してあまり興味を示さないため、子インターフェースの初期化時には親インターフェースの初期化が完了する必要はありません。実際に親インターフェースが使用される(例えば、インターフェースの定数を参照する場合)時のみ、親インターフェースが初期化されます(例えば、インターフェースの定数を参照する場合)。

以下に、クラスのロード全体プロセスを分解します:ロード->検証->準備->解析->初期化

まずはロード:

    仮想機が完了させる必要があるのはこの部分です:3事項:

        1.このクラスの完全限定名を使って、このクラスを定義する二進数バイトストリームを取得します。

        2.このバイトストリームを表す静的ストレージ構造をメソッドエリアのランタイムデータ構造に変換します。

        3.このクラスを表すjava.lang.Classオブジェクトをjava堆で生成し、メソッドエリアのデータへのアクセスエントリとして使用します。

第一点について、非常に柔軟で、多くの技術がここで始まる理由は、バイナリストリームがどこから来たかには制限がありません:

>classファイルから->一般的なファイルロード

zipパックから来た->jarの中のクラスをロード

ネットワークから来た->Applet

..........

他のロードプロセスのステップと比較して、ロードステップは最も制御可能で、クラスローダーはシステムのものか、自分で書いたものを使用できます。プログラマーは、バイトストリームの取得を制御するために自分自身の方法でローダーを書き込むことができます。

バイナリストリームを取得し、完了すると、メソッドエリアにJVMが必要な方法で保存され、java堆にjava.lang.Classオブジェクトがインスタンス化され、堆のデータと関連付けられます。

ロードが完了すると、これらのバイトストリームに対して検証を開始します(実際には多くのステップが上記のものと交差して行われます、例えばファイル形式の検証):

検証の目的:classファイルのバイトストリーム情報がJVMの好みに合致し、JVMが不快を感じないようにすることを確実にします。classファイルが純粋なJavaコードからコンパイルされた場合、配列のオーバーフロー、存在しないコードブロックへのジャンプなどの不健康な問題は発生しません。なぜなら、そのような現象が発生すると、コンパイラはコンパイルを拒否するからです。しかし、先ほど述べたように、ClassファイルストリームはJavaソースコードからコンパイルされたものでなくても、ネットワークや他の場所から来る可能性があります。また、自分自身で16二進数表記では、JVMがこれらのデータに対して確認を行わない場合、有害なバイトストリームがJVMを完全にクラッシュさせることができる可能性があります。

の検証はいくつかのステップを経て行われます:ファイル形式検証->メタデータ検証->バイトコード検証->シンボルリファレンス検証

ファイル形式検証:バイトストリームがClassファイルの形式規格に従っているかどうかを検証し、そのバージョンが現在のJVMバージョンで処理できるかどうかを検証します。okで問題がなければ、バイトストリームはメモリのメソッドエリアに保存されます。後の3の検証はすべてメソッドエリアで行われます。

メタデータ検証:バイトコードで記述された情報に対して文法的な分析を行い、その内容がJava言語の文法規範に従っていることを保証します。

バイトコード検証:最も複雑で、メソッド本体の内容を検証し、実行時に何か異常なことを行わないことを保証します。

シンボルリファレンス検証:を使用して、引用の真実性と実行可能性を検証します。例えば、コード内で他のクラスが参照されている場合、そのクラスが実際に存在するかどうかを検証します。または、コード内で他のクラスの属性にアクセスされている場合、その属性のアクセス可否を検証します。(このステップは後の解析作業の基礎を築きます)

検証段階は重要ですが、必須ではありません。何かコードが繰り返し使用され、信頼性が確認された場合、実施段階では-Xverify:noneパラメータを使用して、クラスのロード時間を短くするために多くのクラス検証手段をオフにします。

上記の手順が完了したら、準備段階に入ります:

この段階では、クラス変数(静的変数を指します)にメモリを割り当てて、類似の初期値を設定します。ここで説明したいのは、このステップでは静的変数にのみ初期値が設定され、インスタンス変数はオブジェクトがインスタンス化されたときに割り当てられます。このクラス変数の初期値設定は、クラス変数の割り当てと少し異なります。例えば以下のようになります:

public static int value=123;

この段階では、valueの値は0ではなく123、なぜなら、この時点ではまだどんなjavaコードも実行されていないからです。123はまだ見えませんが、私たちが見るのは123valueに割り当てるputstatic命令は、プログラムがコンパイルされた後に<clinit>()に存在するため、valueに値を割り当てます。123は初期化時にのみ実行されます。

ここにも例外があります:

public static final int value=123;

ここでは、準備段階でvalueの値が初期化されます。123これは、コンパイル時に行われるもので、javacはこの特別なvalueにConstantValue属性を生成し、準備段階でjmがそのConstantValueの値に基づいてvalueに値を割り当てます。

上の手順を完了したら、解析を行います。解析はクラスのフィールド、メソッドなどのものを変換することのようです。具体的にはClassファイルのフォーマット内容に関連していますが、深く理解していません。

初期化プロセスはクラスロードプロセスの最後のステップです:

前のクラスロードプロセスでは、ユーザーがカスタムクラスローダーを通じて参加できるのはロード段階のみであり、他の動作は完全にjvmが主導しています。初期化に到達すると、実際にjava内のコードを実行し始めます。

このステップではいくつかの前処理が実行されます。準備段階で、既にクラス変数に対してシステム割り当てが一度行われたことを注意してください。

実は、このステップはプログラムの<clinit>();メソッドを実行するプロセスです。以下に<clinit>()メソッドについて研究します:

<clinit>()メソッドは、クラス内のすべてのクラス変数の割り当て動作や静的ステートメントブロック内の文をコンパイラが自動的に収集し、それらの順序がソースファイル内の順序と同じになるように組み合わせて、生成されます。

<clinit>()メソッドはクラスのコンストラクタとは異なり、明示的に親クラスの<clinit>()メソッドを呼び出す必要はありません。仮想機は、子クラスの<clinit>()メソッドが実行される前に親クラスのこのメソッドが既に実行されたことを保証します。つまり、仮想機内で最初に実行される<clinit>();メソッドはjava.lang.Objectクラスのものです。

以下に例を示します:

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

まずSub.Bは静的データに参照を行い、Subクラスが初期化される必要があります。同時に、その親クラスParentがまず初期化動作を行う必要があります。Parentが初期化された後、A=2;つまり、B=2;このプロセスは以下のように相当します:

static class Parent{ 
  <clinit>(){ 
    public static int A=1; 
    static{ 
      A=2; 
    } 
  } 
} 
static class Sub extends Parent{ 
  <clinit>(){ //jvmはまず親クラスの该方法を実行し、その後ここを実行します 
  public static int B=A; 
  } 
} 
public static void main(String[] args){ 
  System.out.println(Sub.B); 
} 

<clinit>();メソッドはクラスやインターフェースにとって必須ではありません。クラスやインターフェースにクラス変数への割り当てがなく、静的コードブロックもない場合、<clinit>()メソッドはコンパイラによって生成されません。

インターフェース内にstatic{}のような静的コードブロックは存在しないが、変数の初期化時の変数割り当て操作が存在するため、インターフェース内にも<clinit>()コンストラクタが生成されます。しかし、クラスとは異なり、サブインターフェースの<clinit>();メソッドを実行する前に、親インターフェースの<clinit>();メソッドを実行する必要はありません。親インターフェースで定義された変数が使用される際に、親インターフェースが初期化されます。

また、インターフェースの実装クラスは、初期化時にインターフェースの<clinit>();メソッドを実行しません。

また、JVMは、多线程環境で<clinit>();メソッドが正しくロックおよび同期されることを保証します。<初期化は一度だけ実行されます>。

以下に例を示します:

public class DeadLoopClass { 
  static{ 
    if(true){ 
    System.out.println("["+Thread.currentThread()+"] イニシャライズしました。次に無限ループを行います"); 
    while(treu){}   
    } 
  } 
  /** 
   * @param args 
   */ 
  public static void main(String[] args) { 
    // TODO Auto-生成されたメソッドスケルトン 
    System.out.println("toplaile"); 
    Runnable run=new Runnable(){ 
      @Override 
      public void run() { 
        // TODO Auto-生成されたメソッドスケルトン 
        System.out.println("["+Thread.currentThread()+"] そのクラスをインスタンス化します"); 
        DeadLoopClass d=new DeadLoopClass(); 
        System.out.println("["+Thread.currentThread()+"] そのクラスの初期化作業が完了しました"); 
      }}; 
      new Thread(run).start(); 
      new Thread(run).start(); 
  } 
} 

ここでは、実行中にブロッキング現象が見られます。

ご読覧ありがとうございます。皆様のサポートに感謝します!

基本チュートリアル
おすすめ