Javaでのクラスのロードプロセス
たとえば、次の簡単なコード
public class HelloWorld {
public static void main(String[] args) {
System.out.println("我已经被加载啦");
}
}
ロードプロセスはどのようなものですか?
完全なプロセスを次の図に示します。
ロードフェーズ
- クラスの完全修飾名を使用して、このクラスを定義するバイナリバイトストリームを取得します
- このバイトストリームで表される静的ストレージ構造をメソッド領域のランタイムデータ構造に変換します
- このクラスのさまざまなデータへのアクセスエントリとして、メモリ内のこのクラスを表すjava.lang.Classオブジェクトを生成します
クラスファイルをロードする方法
- ローカルシステムから直接ロードする
- インターネット経由で取得、典型的なシナリオ:Webアプレット
- zipアーカイブから読み取り、将来的にjarおよびwar形式の基礎になります
- 実行時の計算と生成、最もよく使用されるのは次のとおりです。動的プロキシテクノロジ
- 他のファイルによって生成される、典型的なシナリオ:JSPアプリケーションはプロプライエタリデータベースから.classファイルを抽出しますが、これは比較的まれです。
- 暗号化されたファイルから取得され、クラスファイルの逆コンパイルに対する一般的な保護対策
リンクフェーズ
確認
- 目的は、クラスファイルのバイトストリームに含まれる情報が現在の仮想マシンの要件を満たしていることを確認し、ロードされたクラスの正確性を確認し、仮想マシン自体のセキュリティを危険にさらさないようにすることです。
- これには主に、ファイル形式の検証、メタデータの検証、バイトコードの検証、およびシンボル参照の検証の4つの検証が含まれます。
ツールBinaryViewerを使用してバイトコードを確認します。
不正なバイトコードファイルが表示された場合、検証は失敗します。
準備する
クラス変数にメモリを割り当て、クラス変数のデフォルトの初期値、つまりゼロ値、nullなどを設定します。
public class HelloWorld {
private static int a = 1; // 准备阶段为0,在下个阶段,也就是初始化的时候才是1
public static void main(String[] args) {
System.out.println(a);
}
}
- 上記の変数aには、準備フェーズで初期値が割り当てられますが、1ではなく0です。
- finalで変更された静的なものはここには含まれていません。これは、finalがコンパイル中に割り当てられ、準備フェーズで明示的に初期化されるためです。
- ここでは、インスタンス変数は割り当てられず、初期化されません。クラス変数はメソッド領域に割り当てられ、インスタンス変数はオブジェクトとともにJavaヒープに割り当てられます。
解決する
- 定数プール内のシンボル参照を直接参照に変換するプロセス。
- 実際、初期化が実行された後、解析操作には多くの場合JVMが伴います。
- シンボル参照は、参照されるターゲットを説明するための一連のシンボルです。シンボル参照のリテラル形式は、「Java仮想マシン仕様」のクラスファイル形式で明確に定義されています。直接参照は、ターゲットを直接指すポインター、相対オフセット、またはターゲットに間接的に配置されているハンドルです。
- 解析アクションは、主にクラスまたはインターフェイス、フィールド、クラスメソッド、インターフェイスメソッド、メソッドタイプなどを対象としています。定数プール内のCONSTANTクラス情報、CONSTANT Fieldref情報、およびConstantMethodref情報に対応します。
初期化フェーズ
- 初期化フェーズは、クラスコンストラクターメソッドを実行するプロセスです。このメソッドを定義する必要はありません。javacコンパイラは、クラス内のすべてのクラス変数の割り当てアクションを自動的に収集し、静的コードブロック内のステートメントをマージします(つまり、コードに静的変数が含まれている場合、clinitメソッドがあります)。 。コンストラクターメソッドの命令は、ステートメントがソースファイルに表示される順序で実行されます。
- クラスコンストラクターは、クラスコンストラクターとは異なります。静的変数がない場合、クラス構築メソッド(clinit)は実行されず、クラスが宣言された後、コンストラクターが生成されます。デフォルトは空のパラメーターコンストラクターです。実行クラスのコンストラクターは、initメソッドを実行することです。
public class ClassInitTest {
private static int num = 1;
static {
num = 2;
number = 20;
System.out.println(num);
System.out.println(number); //报错,非法的前向引用
}
private static int number = 10;
public static void main(String[] args) {
System.out.println(ClassInitTest.num); // 2
System.out.println(ClassInitTest.number); // 10
}
}
親クラスが関与する場合の変数割り当てプロセスについて
クラスに親クラスがある場合、JVMは、子クラスの実行前に親クラスが実行されたことを確認します。
public class ClinitTest1 {
static class Father {
public static int A = 1;
static {
A = 2;
}
}
static class Son extends Father {
public static int b = A;
}
public static void main(String[] args) {
System.out.println(Son.b);
}
}
出力結果は2です。つまり、ClinitTest1が最初にロードされると、mainメソッドが見つかり、次にSonの初期化が実行されますが、SonはFatherを継承するため、Fatherの初期化を実行する必要があります。Aは同時に2に割り当てられます。逆コンパイルによってFatherのロードプロセスを取得します。最初に、元の値が1に割り当てられ、次に2にコピーされ、最後に返されることがわかります。
iconst_1
putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A>
iconst_2
putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A>
return
仮想マシンは、クラスの初期化メソッドが同期され、複数のスレッドでロックされていることを確認する必要があります。
public class DeadThreadTest {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 线程t1开始");
new DeadThread();
}, "t1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 线程t2开始");
new DeadThread();
}, "t2").start();
}
}
class DeadThread {
static {
if (true) {
System.out.println(Thread.currentThread().getName() + "\t 初始化当前类");
while(true) {
}
}
}
}
上記のコードの出力は次のとおりです。
线程t1开始
线程t2开始
线程t2 初始化当前类
以上のことから、初期化後の初期化は、同期ロックのプロセスである1回のみであることがわかります。