序文
プログラムがクラスを使用するときに、クラスがメモリにロードされていない場合、JVMは、ロード、リンク、および初期化の3つのステップを通じてクラスをロードします。
Javaクラスファイル
クラスファイルは、8ビットバイトに基づくバイナリストリームのセットであり、各データ項目は、区切り文字なしで順番にクラスファイルに配置されます。したがって、クラスファイル全体に格納されているコンテンツは、プログラムの実行時に必要なほとんどすべてのデータです。8バイトを超えるスペースを占有する必要のあるデータ項目に遭遇すると、最初に上位順に格納するために、データはいくつかの8ビットバイトに分割されます。
JVMナレッジマップを共有する:(私に従って、より多くの乾物を共有してください↓↓↓)
まず、Javaクラスを定義する必要があります。
public class SumDemo {
public static void main(String[] args) {
int a=1;
int b=2;
System.out.println(a+b);
}
}
1.2.3.4.5.6.7.8.
記述されたJavaコードは直接実行できないため、クラスファイルに変換する必要があります。これはコンパイルされます。これを実現するには、JDKに組み込まれているJavaのコマンドペアを使用する必要があります。
たとえば、特定のクラスのバイトコードファイルを生成する場合、クラスファイルを取得するために必要なのはjavacSumDemo.javaのみです。もちろん、実際のプロジェクトでは、通常、javacコマンドを使用して手動でコンパイルすることはありませんが、IDEまたはMaven、Grande、およびその他のツールを使用して、Javaコードをクラスファイルにさらに便利にコンパイルできます。
コンパイルされたクラスファイルはテキストファイルではないため、直接開いて読み取ることはできません。たとえば、notepad ++を使用して開くと、文字化けしたコードであることがわかります。
次に、それを読みたい場合は、javapコマンドを使用して逆コンパイルできます。これはJavaに組み込まれている逆コンパイルツールです。使用方法を見てみましょう。
次の図
に示すように、以下はjavap-v-pで逆コンパイルして生成されたファイルです。クラスファイルは逆コンパイルされているため、この時点で.javaについていく必要はないことに注意してください。これにより、クラスファイルの内容を確認できます。次のコマンドを使用して.txtファイルに出力することもできます:javap -v -pxxx>xxx.txt。
E:\JavaSpace\Java-Prepare-Lesson\Java-High\JVM-Frame\JVM-Chapter01-Demo\src\main\java\com\itbbfx>javap -v -p SumDemo
警告: 二进制文件SumDemo包含com.itbbfx.SumDemo
#描述信息
Classfile /E:/JavaSpace/Java-Prepare-Lesson/Java-High/JVM-Frame/JVM-Chapter01-Demo/src/main/java/com/itbbfx/SumDemo.class
Last modified 2021-5-16; size 409 bytes
MD5 checksum b9b13ea5dba3f2b62f4764d30eafc7fc
Compiled from "SumDemo.java"
#描述信息
public class com.itbbfx.SumDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
#常量池
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // com/itbbfx/SumDemo
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 SumDemo.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 com/itbbfx/SumDemo
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
#字段信息
public com.itbbfx.SumDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
#方法信息
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: iload_1
8: iload_2
9: iadd
10: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
13: return
LineNumberTable:
line 18: 0
line 19: 2
line 20: 4
line 21: 13
}
SourceFile: "SumDemo.java"
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.
逆コンパイル後の結果を分析すると、クラスファイルにいくつかの部分が含まれていることがわかります。最初の部分は、クラスの説明情報です。クラスファイルが保存されている場所、変更されたときのクラスファイルのMD5値、およびクラスファイルがコンパイルされたJavaクラスを記録します。2番目の部分は、いくつかの説明情報です。これは主に、このクラスがコンパイルされるJDKのバージョンを説明します。メジャーバージョン:52はJDK8を意味します。3番目の部分は定数プールです。4番目の部分はフィールド情報です。最後に、5番目の部分はメソッドの情報です。実際、ファイル全体にさまざまな指示があり、これらの指示を直接理解することは困難です。
クラスファイルのロード
では、JVMはどのようにクラスファイルをロードするのでしょうか。クラスが作成または参照されるときに、仮想マシンがクラスが以前にロードされていないことを検出すると、クラスローダー(ClassLoader)を介してクラスファイルをメモリにロードします。ロードのプロセスでは、主にクラスがロードされます。 3つのことをします。
最初に:クラスのバイナリストリームを読み取り、
2番目:バイナリストリームをメソッド領域のデータ構造に変換し、データをメソッド領域に格納します。
最後に、java.lang.ClassオブジェクトがJavaヒープに生成されます。
.classファイルをロードする方法:
- ローカルシステムから直接ロードする
- ネットワークを介して取得、典型的なシナリオ:Webアプレット
- zip圧縮ファイルからの読み取りは、将来のjarおよびwar形式の基礎になります。
- 実行時の計算生成、最もよく使用されるのは次のとおりです。動的プロキシテクノロジ
- JSPアプリケーションなどの他のファイルから生成
- 独自のデータベースから.classファイルを抽出します。これは比較的まれです。
- 暗号化されたファイルから取得され、クラスファイルの逆コンパイルに対する一般的な保護対策
リンク
ロードが完了すると、リンクステップに入ります。リンクステップは、検証、準備、および解析に細分化できます。
確認
検証して理解することは、クラスファイルが仕様に準拠しているかどうかを検証し、ロードされたクラスの正確性を確認することであり、仮想マシン自体のセキュリティを危険にさらすことはありません。これには、主に4種類の検証、ファイル形式の検証、メタデータの検証、バイトコードの検証、およびシンボル参照の検証を含む、複数のレベルの検証が含まれます。
ファイル形式の検証:たとえば、ファイルが0xCAFEBABEで始まるかどうかに関係なく、16進エディターを使用してファイルを開いて表示できます。たとえば、Beyond Compareを使用してSumDemo.classファイルを開き、確認します。他の16進エディタを使用して開いて表示することもできます。
ここでは、CAFEBABEの後にモジュラスと呼ばれる数値が続くことがわかります。
メタデータの検証:たとえば、このクラスに親クラスの検証があるかどうか、このクラスがfinalを実装しているかどうかを確認します。これは、finalクラスを継承できないためです。たとえば、非抽象クラスです。すべての抽象メソッドが実装されているかどうかにかかわらず、実装されていない場合、このクラスも無効です。
バイトコード検証:バイトコード検証は非常に複雑です。クラスファイルがバイトコード検証に合格できるという事実は、クラスに問題がないことを意味するものではありません。ただし、バイトコードの検証に合格しない場合は、問題があるはずです。バイトコードの検証は、主に、実行チェック、データ型、オペコード操作などのパラメータが一致するかどうかによって決定されます(たとえば、スタックスペースは2バイトだけですが、実際には2バイトより大きくする必要があります。現時点では、このワードセクションコードには問題があると考えられます)、ジャンプ命令が適切な場所を指しているかどうかなどです。
シンボリック参照の検証:次に、シンボリック参照がここにあるかについては説明しません。後で誰もがそれを知るようになります。シンボリック参照の検証には、定数プール内の記述クラスが存在するかどうか、アクセスメソッドまたはフィールドが存在するかどうか、および十分な権限があるかどうかの検証も含まれます。コードが安全で正しいことを事前に確認した場合。次に、起動時にそのようなパラメータを追加できます。-Xverify:none、検証をオフにするには、クラスのロードを高速化します。
例:IDEAのヘルプ->カスタムVMオプションの編集。
ここでは、IDEAの起動パラメータを表示できます。起動パラメータに項目-Xverify:noneがあります。
このように、IDEAの起動時に検証が実行されないため、IDEAの起動が高速化されます。このオプションをEclipseまたは他のIDEに設定して、IDEAの起動を高速化することもできます。
さて、検証手順は非常に詳細であることがわかりますが、初心者の場合は詳細にあまり注意を払わないことをお勧めします。これは、Javaクラスが正常であるかどうかを確認する手順として理解できます。 。検証され、クラスファイルに問題がないことが判明した場合は、準備段階に入ります。
準備
準備フェーズは、クラス変数(静的変数)にメモリを正式に割り当て、初期値を設定するフェーズです。これらの変数で使用されるメモリは、メソッド領域に割り当てられます。
このリンクの役割は、クラスの静的変数にメモリを割り当て、システムの初期値に初期化することです。次に、final staticによって変更された変数の場合、変数には、準備プロセスで直接値が割り当てられます。これは、ユーザー定義の値です。たとえば、private final static int value = 123456のように定義すると、この段階で123456が値に直接割り当てられます。
ただし、この段階の静的変数の場合、その値はまだ0です。たとえば、private static int value = 123456、この段階の値はまだ0であり、123ではありません。Javaメソッドがまだ実行されていないため、変数をに割り当てます。 123456、yesは初期化フェーズ中に実行されます)。準備が完了したら、分析を入力できます。
解析
シンボリック参照はリテラル参照であり、仮想マシンの内部データ構造やメモリ分散とは関係ありません。Classクラスファイルでは、定数プールを介して多数のシンボリック参照が作成されることは比較的簡単に理解できます。しかし、プログラムが実際に実行されているときは、シンボリック参照だけでは不十分です。
解析の役割は、シンボリック参照を直接参照に変換することです。いわゆるシンボリック参照とは、コンパイル時に、Javaクラスが参照されるオブジェクトの実際のアドレスをまだ認識していないことを意味します。だから、私が今誰を参照したいかを言うために記号を使うだけです。たとえば、クラスファイルの定数プールには、カーネルインターフェイスの完全修飾名、メソッドの参照、メンバー変数の参照など、すべてのシンボリック参照が格納されます。したがって、これらのクラス、メソッド、または変数を実際に参照する場合は、これらのシンボルを、検出可能なオブジェクトポインターまたはアドレスオフセットに変換する必要があります。変換された参照は直接参照です。
たとえば、次のprintln()メソッドが呼び出されると、システムはメソッドがどこにあるかを正確に知る必要があります。例:操作System.out.println()に対応するバイトコードを出力します。
それがより一般的である場合、シンボリック参照は、現在参照される人であるマークが作成されることを意味し、直接使用は実際にオブジェクトを参照することです。解析が完了すると、初期化フェーズに入ります。
初期化
要するに、初期化フェーズでは、クラスの静的変数に正しい初期値が割り当てられます。クラスの初期化は、クラスの読み込みの最終段階です。前の手順で問題がなければ、プレゼンテーションクラスをシステムにスムーズにロードできます。この時点で、クラスはJavaバイトコードの実行を開始します。(つまり、初期化フェーズまで、クラスで定義されたJavaプログラムコードの実際の実行が開始されます)。
初期化フェーズの重要な作業は、クラス:()メソッドの初期化メソッドを実行することです。JVMが最初に実行するメソッド。この方法では、コンパイラーがクラス内のすべての静的変数を自動的に収集し、その割り当てアクションと静的ステートメントがすばやくマージされます。これは、クラスコンストラクターメソッドとも呼ばれます。
メソッドのコード実行順序は、ソースファイルの順序と同じです。例を実行して見てみましょう。ここで実行してみましょう。
static int a=1;
static {
a = 3;
}
public static void main(String[] args) {
System.out.println(a);
}
1.2.3.4.5.6.7.8.
出力結果は次のとおりです。3。
静的変数を選択したことがわかります。ここには別の静的コードブロックがあり、最初に値1を割り当て、次に値3を割り当てています。のメソッドは、これら2つのコードを1つのメソッドに結合するため、aの最終的な値は3になります。次に、順序をもう一度調整します。実行が成功すると、出力結果は1になります。
サブクラスのメソッドは、スーパークラスのメソッドが呼び出される前に呼び出されます。
JVMは、メソッドのスレッドセーフを保証します。
また、JVM初期化コードを実行する際に、新しいオブジェクトをインスタンス化すると、メソッドが呼び出され、インスタンス変数が初期化され、対応するコンストラクターのコードが実行されます。もう一度例を実行してみましょう。
static {
System.out.println("JVM 静态块");
}
{
System.out.println("JVM 构造块");
}
public SumDemo(){
System.out.println("JVM 构造方法");
}
public static void main(String[] args) {
System.out.println("main");
new SumDemo();
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.
次に、このクラスには、静的コードブロック、構築ブロック、構築メソッド、およびメインメソッドがあります。では、実行の順序は何ですか?それを実行して見てください。
静的コードブロックが最初に実行され、次にmainメソッドが実行され、次にコンストラクターが実行され、最後にコンストラクターが実行されることがわかります。
この例をもう一度見てみましょう。
static {
System.out.println("JVM 静态块");
}
{
System.out.println("JVM 构造块");
}
public SumDemo(){
System.out.println("JVM 构造方法");
}
public static void main(String[] args) {
new Sub();
}
public class Super {
static {
System.out.println("Super 静态块");
}
public Super(){
System.out.println("Super 构造方法");
}
{
System.out.println("Super 构造块");
}
}
public class Sub extends Super {
static {
System.out.println("Sub 静态块");
}
public Sub(){
System.out.println("Sub 构造方法");
}
{
System.out.println("Sub 构造块");
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.
この場合、mainメソッドはSubオブジェクトをインスタンス化し、SubクラスはSuperから継承します。この場合、SumDemo、Supper、Subにはすべて静的コードブロック、構築メソッド、構築ブロックがあり、それらを実行してコンソールの出力を確認します。
JVMの静的コードブロックが最初に出力され、コンストラクターとコンストラクターのコードが実行されていないことがわかります。新しいSumDemoに移動しなかったため、これは簡単に理解できます。次に、コードがmainメソッドに対して実行されると、新しいSubオブジェクトに移動します。newの前に、ここでメソッドが実行されるため、Subクラスのコードが実行されます。
ただし、サブクラスのメソッドが呼び出される前に、親クラスのメソッドが呼び出されるため、最初にSuperクラスの静的メソッドを呼び出し、次にSubクラスの静的メソッドを呼び出します。通話が完了しました。再度コンストラクションブロックと呼びます。コンストラクションブロックの実行順序は、コンストラクションメソッドの前であることがわかります。最初にスーパークラスのコンストラクションブロックが実行され、次にスーパークラスのコンストラクションメソッドが実行され、最後にコンストラクションが実行されます。 SubクラスのブロックとSubクラスの構築が実行されます。メソッド。
使用およびアンインストール
タイプを使用する前に、ロード、リンク、および初期化の3つのクラスロードステップをすべて実行する必要があります。タイプがこれらの3つのステップを正常に完了すると、「すべての準備が整い、風だけが残ります」と、開発者がアクセスしてプログラムで呼び出すことができる静的クラスメンバー情報を開発者が使用するのを待ちます(たとえば、 :静的フィールド、静的メソッド)、またはnewキーワードを使用してそのオブジェクトインスタンスを作成します。
Java仮想マシンに付属するクラスローダーによってロードされたクラスは、仮想マシンのライフサイクル中にアンロードされることはありません。Java仮想マシン自体は常にこれらのクラスローダーを参照し、これらのクラスローダーは常にロードするクラスのClassオブジェクトを参照するため、これらのClassオブジェクトは常に到達可能です。ユーザー定義のクラスローダーによってロードされたクラスは、アンロードできます。
スタートアップクラスローダーによってロードされたタイプは、実行時にアンロードできません(JVMおよびJLS仕様)。
システムクラスローダーおよび標準拡張クラスローダーによってロードされたタイプは、実行時にアンロードされる可能性が低くなります。これは、システムクラスローダーインスタンスまたは標準拡張クラスのインスタンスが、実行時に常に直接または間接的にアクセスできるためです。これは非常にアクセスしやすいです。到達不能になる可能性は低いです。(もちろん、仮想マシンが終了しようとしているときに実行できます。これは、ClassLoaderインスタンスまたはClass(java.lang.Class)インスタンスがヒープ内に存在するかどうかに関係なく、ガベージコレクションのルールにも準拠しているためです) 。
開発者定義のクラスローダーインスタンスによってロードされる型は、非常に単純なコンテキストでのみアンロードでき、通常は仮想マシンのガベージコレクション機能にアンロードするように強制します。多くのアプリケーションシナリオでは、もう少し複雑になることが予想されます。 (特に多くの場合、ユーザーはカスタムクラスローダーインスタンスを開発するときにシステムパフォーマンスを向上させるためにキャッシュ戦略を使用します)、ロードされたタイプが実行時にアンロードされる可能性はほとんどありません(少なくともアンロード時)。
さて、初期化が完了したら、このクラスを使用できますが、使用しない場合はアンロードできます。これは理解しやすいです。次の図は、より一般的なクラスのロードプロセスです。実際、クラスがロードされるとき、それは必ずしもこのプロセスに完全に従うとは限りません。
たとえば、解析は必ずしも初期化の前に行われるとは限らず、初期化の後に行われる場合があります。
最後に
ここに、Java(jvm)仮想マシンに関連するドキュメントをまとめました。Springシリーズのファミリバケット、Javaの体系的な情報:( Javaのコアナレッジポイント、インタビュートピック、最新の21年間のインターネットの実際の質問、e-本など)困っている友達は公式アカウント[ProgramYuanXiaowan]をフォローして入手できます。