記事のディレクトリ
クラスローディングメカニズムとクラスローダー—Java仮想マシン
Javaクラスファイルをコンパイルすると、クラス名.classのバイトコードファイルになりますが、このときの.classファイルは、メモリ内のメソッド領域に追加した場合のみ、すぐに実行することはできません。検証、変換、分析、初期化されると、最終的にはJava仮想マシンで直接使用できるJavaタイプになります。このプロセスは、Java仮想マシンのクラスロードメカニズムと呼ばれます。2つの図で視覚的に理解しましょう。位置JVMアーキテクチャ全体でのクラスロードプロセスの概要。
ご覧のとおり、JVMアーキテクチャには、このプロセスを説明するための専用のクラスローディングサブシステムがあります。全員が理解する価値のある2つの部分があります。
- .classファイルはどのようにロードされますか:これはクラスロードメカニズムです
- クラスファイルのロードの責任:これはクラスローダーです
次の2つの側面から、クラスローディングサブシステムを詳しく見てみましょう。
1.クラスローディングメカニズム:
Java言語では、型のロード、接続、および初期化は、プログラムの実行プロセス中に完了します。これにより、Java言語で少し余分なオーバーヘッドが発生しますが、Java参照に非常に高いスケーラビリティと柔軟性が提供されます。Javaは本質的に動的に拡張可能な言語機能は、実行時の動的ロードと動的接続に依存することで実現されます。
タイプは、仮想マシンのメモリにロードされることから始まり、メモリからアンロードされるまで、ライフサイクル全体が7つの小さな段階を経ます。
- 読み込み中
- 検証
- 準備(準備)
- 解決
- 初期化(初期化)
- 使用(使用)
- 荷降ろし
検証、準備、分析の3つの段階をまとめてリンクと呼びます。ロード、検証、準備、初期化、アンロードの5つのプロセスの順序が決定されます。次に、クラスのライフサイクルの概略図を示します。
1.1ロード
ロードはClassLoadingのプロセスにすぎません。混同しないでください。ロードフェーズでは、Java仮想マシンは次の3つのことを完了する必要があります。
- クラスの完全修飾名を使用してクラスを定義するバイナリバイトストリームファイルを取得します。たとえば、誰もがよく使用するStringクラスの完全修飾名はjava.lang.Stringです。
- バイトストリームで表される静的ストレージ構造をメソッドエリアのランタイムデータ構造に変換します。クラスファイルは主にメソッドエリアにロードされます。
- このクラスを表すjava.lang.Classオブジェクトは、メソッド領域(リフレクションメカニズム)内のこのクラスのさまざまなデータへのアクセスエントリとしてメモリに生成されます。さまざまな操作は、リフレクションメカニズムを介して完了できます。
1.2検証
検証は接続フェーズの最初のステップです。このフェーズの目的は、クラスバイトストリームの情報が<java仮想マシン仕様>のすべての要件を満たしていること、およびその後のJVMに害を及ぼさないことを確認することです。実行されます。
1.3準備(準備)
準備段階は、メモリを正式に割り当て、**クラスで定義されたゼロ値(最終的に変更されたものを除く)変数(静的で変更された静的変数、クラス変数とも呼ばれます)を設定する段階です。インスタンス変数(つまり、役に立たない)静的に変更された変数)**特定のオブジェクトがインスタンス化されるまで、Javaヒープに割り当てられます。
例えば:
private static int a=1;//准备阶段分配内存,并且赋初始值0.初始化阶段初始化为1
private int b=2;//new 一个新对象的时候才在堆中分配内存并赋值
private final static int c=3;//准备阶段就会分配内存,并且赋初始值3
つまり、クラス変数は、準備フェーズのコードの初期値に従って割り当てられるのではなく、初期化フェーズまで待機します。ただし、finalで変更すると、クラス変数は異なります。変数が変更されたためです。 with finalは、javacがConstantValueプロパティを生成するときにコンパイルされるため、準備フェーズでは、ConstantValueに従って割り当てられます。
1.4解像度
解析フェーズは、JVMが定数プール内のシンボリック参照を直接参照に置き換えるプロセスです。
1.5初期化(初期化)
クラスが初期化されるときのクラスロードプロセスの最後のアクション。前のいくつかのプロセスでは、ロードフェーズ中のユーザー定義ローダーの部分的な参加を除いて、残りはJVMによって制御されます。初期化フェーズまで、 JVMは、実際には、クラス内のプログラマーによって記述されたJavaプログラムコードの実行を開始し、アプリケーションをリードします。
準備段階では、クラス変数にゼロ値が割り当てられています(定数を除く)が、初期化段階では、クラス変数やその他のリソースはプログラマーのコーディングに従って初期化されます。別のより直感的な観点から表現することもできます:初期化ステージは、クラスコンストラクターの<clinit>()メソッドを実行するプロセスです。<clinit>()はプログラマーによって作成されませんが、Javacはクラス内のすべての割り当てアクションと静的ステートメントブロックを自動的に収集します(static( ))<clinit>()のステートメントは、ステートメントがソースファイルに表示される順序です。
たとえば、コードを記述し、Jclasslibプラグインを介してバイトコードファイルを表示します。
public class ClinitTest
{
private int a=0;
private static int b=1;
static
{
b=2;
}
}
Jclasslibビューの結果は次のとおりです。
ご覧のとおり、bの値は最初に1に順番に割り当てられ、次にbの値が2に割り当てられます。インスタンス変数aはありません。
<clinit>メソッドに関するいくつかの注意事項がありますが、ここでは説明しません。
- このクラスの<clinit>メソッドが呼び出される前に、JVMは親クラスの<clinit>が実行されている必要があることを確認します。つまり、Objectクラスの<clinit>が最初に実行される必要があります。
- クラスの<clinit>メソッドは必要ありません。クラス変数と静的コードブロックのコピーがクラスにない場合、<clinit>メソッドはこのクラスに対して生成されません。
- 静的コードブロックはインターフェイスで使用できませんが、変数を宣言できるため、<clinit>メソッドも生成できますが、親インターフェイスの<clinit>メソッドは、インターフェイスの<clinit>が実行される前に実行されません。そして、インターフェースの実装クラスが初期化されています。インターフェースの<clinit>メソッドは、プロセス中に実行されません。
- クラスは一度しか初期化できないため、<clinit>は正しくロックおよび同期されます。
2.クラスローダー(ClassLoader)
ロードフェーズで「クラスの完全修飾名を介してクラスを記述するバイナリバイトストリームを取得する」アクションを実行するために使用されるコードは、「クラスローダー」と呼ばれます。
クラスローダー構造の概略図:
多くの人は、クラスローダーがクラスロード段階の1つの段階にすぎないことを理解しています。これを小さな関数にすることはできません。なぜ、特別な本を予約するために特別な部分として扱う必要があるのでしょうか。
実際にはそうではありません。Javaプログラムでは、クラスのロード段階をはるかに超えて役割を果たします。どのクラスでも、クラスをロードするクラスローダーとクラス自体がJVMでの一意性を判断する必要があります。各クラスローダーはすべて独立したクラス名前空間を持っています。より一般的な言語では:
2つのクラスが等しいかどうかを判断するための条件:(同じクラスファイルから&&同じクラスローダーによってロードされます)
2.1クラスローダーの種類(JDK1.8)
JVMの観点からは、クラスローダーには2つの主要なタイプしかありません。
- Bootstrap ClassLoader:C ++言語で実装され、JVM自体の一部です。
- その他のクラスローダー:抽象クラスjava.lang.ClassLoaderから継承された、仮想マシンから独立したJava言語によって実装されます。
しかし、Java開発者の観点からは、クラスローダーはより詳細です。Javaは常に3層のクラスローダーと親委任クラスローディングアーキテクチャを維持してきました。
詳細な分類については、4つのタイプに分類でき、次のように優先度でソートされます。上位ローダーは、次のローダーの親クラスローダーです。
- Bootstrap ClassLoader(Bootstrap ClassLoader)
- 拡張クラスローダー
- アプリケーションクラスローダー(アプリケーションクラスローダー):システムクラスローダーとも呼ばれます
- ユーザー定義のClassLoader(UserDefined ClassLoader)
2.2 Bootstrap ClassLoader(Bootstrap ClassLoader)
スタートアップクラスローダーはC ++コードで記述されており、主に以下をロードするために使用されます。
- rt.jarなどの<JAVA_HOME> \ libのコアクラス、tools.jarのクラス
- または、JVMパラメーターVMオプションで指定されたクラス:-Xbootclasspathパラメーター
ClassLoader classLoader = String.class.getClassLoader();//结果是null,但是不意味着不存在
結果はnullですが、C ++で記述されているため、存在しないという意味ではありません。Javaレベルで表すことはできず、結果はnullです。JVMクラスローダーでは、nullを直接使用できます。 BootstrapClassLoaderを参照します。
2.3拡張クラスローダー(拡張クラスローダー)
このクラスローダーは、クラスsun.misc.Launcher $ ExtClassLoaderのJavaコードで実装され、以下のロードを担当します。
- <JAVA_HOME> \ lib \ extディレクトリ
- または、java.ext.dirsシステム変数で指定されたパス内のすべてのクラスライブラリ
ClassLoader classLoader = SunEC.class.getClassLoader();
//结果是sun.misc.Launcher$ExtClassLoader@72ea2f77
System.out.println(classLoader);
2.4アプリケーションクラスローダー(アプリケーションクラスローダー)
このクラスローダーは、sun.misc.Launcher $ AppClassLoaderによって実装されます。これは、ユーザークラスパス(ClassPath)上のすべてのクラスライブラリ、つまり、ユーザーによって作成されたクラスとサードパーティのクラスライブラリをロードする役割を果たします。
//springboot是第三方的类库
ClassLoader classLoader = SpringBootCondition.class.getClassLoader();
//结果是sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader);
//ClassLoaderTest是我自己定义的一个类类
ClassLoader classLoader1 = new ClassLoaderTest().getClass().getClassLoader();
//结果是sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader1);
//通过ClassLoader.getSystemClassLoader()可以获取ApplicationClassLoader
ClassLoader ClassLoader3 = ClassLoader.getSystemClassLoader();
//结果是sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(ClassLoader3);
また、ClassLoaderの静的メソッドgetSystemClassLoader()の戻り値であるため、「システムクラスローダー」とも呼ばれます。
2.5ユーザー定義のClassLoader(UserDefined ClassLoader)
デフォルトでは、Javaは3種類のローダーを提供しますが、特別な状況では、プログラマーは独自のクラスローダーをカスタマイズでき、ClassLoaderインターフェースを実装するだけで済みます。
3.親委任モデルとその破壊
3.1親の委任モデル
Javaでのクラスのロードでは、動的ロードが使用されます。つまり、クラスが使用されている場合にのみ、対応するバイトコードファイルがロードされます。
クラスローダーがクラスロード要求を受信すると、すぐにはロードされませんが、この要求はに委任されます。親クラスローダー。最上位のブートストラップローダーに到達するまで。
親クラスローダーがクラスのロードプロセスを完了できる場合、このクラスのロードは親クラスに渡されて完了します。それ以外の場合は、に委任されます。そのサブクラスローダー。
以下は、親委任モデルの実装です。
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//1 首先检查类是否被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2 没有则调用父类加载器的loadClass()方法;
c = parent.loadClass(name, false);
} else {
//3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
//4 若父类加载失败,抛出ClassNotFoundException 异常后
c = findClass(name);
}
}
if (resolve) {
//5 再调用自己的findClass() 方法。
resolveClass(c);
}
return c;
}
3.2親委任モデルの利点
-
クラスの繰り返しロードを回避する:Javaのクラスとそのクラスローダーは、優先度と階層関係にあります。たとえば、java.lang.Objectクラスは、どのクラスローダーに関係なく、Javaのコアクラスです。このクラスをロードすると、最終的には最上位のBootstrapクラスによってロードされるため、異なる環境のオブジェクトが同じクラスであることが保証されます(同じクラスには2つの側面があるため)。
-
プログラムのセキュリティを保護し、コアAPIが任意に改ざんされないようにします。プログラミングでは、サードパーティのライブラリまたは自己定義クラスを使用することがよくあります。完全修飾名がプログラムに表示される場合
java.lang.Objectのクラスの場合、親委任モデルがない場合、複数のObjectクラスが表示され、プログラムが混乱します。
3.3親の委任モデルの破壊
親委任モデルは、必須の制約があるモデルではなく、Java設計者が開発者に推奨するクラスローダーの実装です。Javaの世界では、ほとんどのクラスローダーがこのモデルに従いますが、例外があります。Javaモジュール化(JDK9)が登場するまで、合計3つの大規模な「破壊された」状況がありました
-
最初の「壊れた」は、親委任モデルがJDK1.2で導入されたため、親委任モデル(JDK1.2)の前に表示されましたが、ClassLoaderはJDK1.1にすでに存在していました。したがって、このモデルが壊れた前。JDK1.2の後親委任モデルが導入されましたが、既存のユーザー定義クラスローダー(主にloadClassメソッドの書き換え)に直面して、いくつかの妥協が必要でした。親委任モデルはメソッドのloadClass()とJDK1に反映されるためです。 1人のユーザーがloadClass()メソッドを書き直したため、モデルが破棄されました。そのため、デザイナーはjava.lang.ClassLoaderクラスに新しい保護されたメソッドfindClass()を定義しました。ガイドユーザークラスの読み込みメカニズムを作成するときに、このメソッドをオーバーライドします。モデルが破壊されないこと。
次のコードはloadClassメソッドです。そのロジックは親委任モデルです。2番目のif(c == null)コードを確認できます。Javaの3つのデフォルトクラスローダーのいずれもクラスのロードを完了しない場合ユーザー定義の実行findClass()。これは、ユーザーが希望どおりにクラスをロードするのに影響を与えず、親委任モデルを破棄しません。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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 thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);//程序员重写findClass方法这样就不会破坏模型
// 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;
}
}
2. 2番目の損傷は、それ自体の欠陥によって引き起こされます。親委任モデルは、各クラスローダーが連携する場合の基本タイプの一貫性の問題を解決します(より基本的なタイプは、上位クラスのローダーLoadによって決定されます)。タイプはユーザーのコードを呼び出す必要があります。どうすればよいですか?言い換えると、プログラマーは独自のコードを使用して非常に基本的なインターフェイスを実装しています。つまり、この非常に基本的なインターフェイスをロードするにはBootstrap ClassLoaderで作成する必要がありますが、クラスローダーはユーザーのコードを認識しませんか?どうすればよいですか?
このジレンマを解決するために、Java設計チームはあまりエレガントではない設計を導入する必要がありました:スレッドコンテキストクラスローダー(スレッドコンテキストクラスローダー)。このクラスローダーは、java.lang.ThreadクラスのsetContextClassLoader()メソッドで設定できます。スレッドの作成時に設定されていない場合は、親スレッドから継承されます。グローバルで設定されていない場合は、アプリケーションのスコープの場合、このクラスローダーはデフォルトでアプリケーションクラスローダーです。つまり、高度なクラスローダーはスレッドファイルクラスローダーを使用してユーザーのコードをロードします。これにより、実際には親委任モデルが破損します。
3.親委任モデルの3番目の「破壊」は、ユーザーがプログラムのダイナミクスを追求したことが原因でした。IBMはこのためにOSGiを立ち上げました。
OSGiのモジュラーホットデプロイメントの中核は、カスタムクラスローダーメカニズムの実現にあります。すべてのプログラムモジュール(バンドル)には独自のクラスローダーがあります。バンドルを置き換える必要がある場合は、バンドルをクラスローダーと一緒に置き換えて、ホットコードの置き換えを実装します。OSGiの錯覚では、クラスローダーは親委任モデルのツリー構造ではなくなりましたが、さらに複雑なネットワーク構造に発展しました。クラスの読み込み要求を受信すると、OSGiは次の順序でクラス検索を実行します。 :
- java。*で始まるクラスを親クラスローダーに割り当ててロードします。
- それ以外の場合は、デリゲートリスト内のクラスを親クラスローダーに割り当ててロードします。
- それ以外の場合は、インポートリストのクラスをエクスポートクラスのバンドルのクラスローダーに委任してロードします。
- それ以外の場合は、現在のバンドルのClassPathを見つけて、独自のクラスローダーでロードします。
- それ以外の場合は、クラスが独自のフラグメントバンドルに含まれているかどうかを確認し、含まれている場合は、フラグメントバンドルのクラスローダーに委任してロードします。
- それ以外の場合は、[動的インポート]リストでバンドルを見つけ、ロードするバンドルに対応するクラスローダーに委任します。
- それ以外の場合、クラスローダーは失敗します。
コードワードは簡単ではありませんが、上手に書く人のためにスリーインワンを教えていただけますか?