Javaクラスローディング機構
詳細なJavaのクラスローディング機構。@pdai
クラスのライフサイクル
含まれるクラスのロード処理加载
、验证
、准备
、解析
、初始化
5つのステージを。これらの5つのステージでは、加载
、验证
、准备
および初始化
これらの4つのフェーズの順序を決定し、されて発生し、解析
必ずしも、それが初期化フェーズの後に開始することができますされていない段階を結んだ、いくつかのケースでは、これは、Java言語ランタイムをサポートすることです(また、動的バインディングまたは遅延バインディングと呼ばれる)のセット。これらの段階は、一般に別の混合は、典型的に呼び出すか実行中に別の段階でステージを活性化するものを交差するためにも、むしろ完全にまたは連続的により開始シーケンスのいくつかの段階が存在することに注意してください。
クラスのロード:バイナリデータのクラスを見つけ、ロード
第一段階は、ロード・フェーズ中にクラスのロードをロードし、仮想マシンは、3つのことを実行する必要があります。
- クラスの完全修飾名で定義されたバイナリバイトストリームを取得します。
- これは、ランタイムデータ構造領域法に静的記憶構造のバイトストリームを表します。
- このようなデータ領域のエントリへのアクセス方法として、このクラスのJavaヒープのjava.lang.Classオブジェクトの代表を生成します。
クラスローディングの他のステージに比べて、ローディング・フェーズは(具体的には、操作は、クラスローディング・フェーズを取得バイトのバイナリストリームである)制御最強段階で、開発者は、両方のクラスローダを提供するためにシステムを使用することができるので、読み込みが完了している、あなたはまた、読み込みが完了するために、独自のクラスローダをカスタマイズすることができます。
ロード・フェーズは、メソッドの面積に応じて仮想マシンに必要なバイトのバイナリストリーム上の仮想マシンの外部ストレージフォーマットを完了したが、また、Javaでヒープを作成した後java.lang.Class
、あなたがターゲットゾーン法を介してアクセスできるように、オブジェクトクラスこれらのデータ。
クラスローダは、それがロードされるまで待つ必要はありません、JVM仕様クラスが使用されることが期待されている場合、クラスは、「最初のアクティブな使用は」クラスローダを可能にしたときに、プリロードの過程で遭遇した場合、それは、それが事前にロードされています.classファイルが見つからないか、エラー、クラスが最初のクラスは、積極的にプログラムを使用していない場合、クラスローダがエラーを報告しませんプログラムでエラー報告(のLinkageErrorエラー)を使用するためのイニシアチブをとる必要があるクラスローダがあります。
負荷の.classは道をファイル
- ローカルシステムから直接ロード
- ネットワーク経由でダウンロードした.classファイル
- などのzip、jarアーカイブから.classファイルをロードします
- 独自のデータベースからファイルを抽出した.class
- 動的に.classファイルにコンパイルJavaソースファイル
接続
検証:ロードされたクラスの正確さを保証
最初のステップは、接続フェーズを確認することです、この段階の目的は、クラスに含まれる情報のバイトストリームファイルが自分自身の安全を危うくしません、現在の仮想マシンと仮想マシンの要件を満たしていることを確認することです。検証フェーズは、検査操作を大まかに4つの段階を完了されます。
文件格式验证
:クラスファイル形式に準拠したバイトストリームを確認します。たとえば:するかどうか0xCAFEBABE
で始まり、定数プールの種類の定数がある場合、仮想マシンの現在の処理範囲でメジャーとマイナーバージョン番号が、サポートされていませんか。元数据验证
:バイトコード情報は、意味解析(注:比較説明javac
コンパイルの意味分析フェーズ)は、Java言語仕様の要件の遵守を確実にするための情報を記述し、;例えば:親クラスかどうか、に加えて、java.lang.Object
外部。字节码验证
:プログラムのセマンティクスを決定するために、データ・フローと制御フロー解析は、論理的な正当なものです。符号引用验证
:分析操作が適切に行われていることを確認してください。
検証フェーズは非常に重要であるが、必ずしもそうではない、プログラムの実行には影響を与えません、参照されるクラスは後に検証を繰り返した場合、使用を検討して
-Xverifynone
仮想マシンのクラスローダを短縮するために、クラスの検証措置のほとんどをシャットダウンするパラメータを時間。
調製:クラス静的変数のメモリを割り当て、そのデフォルト値を初期化します
準備フェーズは、正式にクラス変数にメモリを割り当て、ステージクラス変数の初期値に設定され、このメモリは、メソッド領域に割り当てられます。次のポイントは、この段階に注意を払う必要があります。
- この時間は、(専用メモリ割り当てクラス変数を含む
static
)、およびオブジェクトがインスタンス化されるときにJavaオブジェクトとして割り当てられたヒープ内のインスタンス変数、インスタンス変数を含んでいません。 典型的には、ここで設定した初期値がゼロ(例えば、デフォルト値のデータ型である
0
、0L
、null
、false
値はJavaコードで明示的に与えられているのではなく、等)。:クラス変数は次のように定義されていると
public static int value = 3
準備段階後の変数値、初期値、0
代わりに、3
このときはまだJavaメソッドを開始していない、および3に割り当てられた値から、put static
プログラム後の命令がコンパイルされ、Aに格納されていますクラスのコンストラクタ<clinit>()
メソッドで、SO 3のみ初期化フェーズで行われる操作に割り当てられた値。
以下の点に注意を払う必要があります
- クラス変数のための基本的なデータ型、(静的)と、グローバル変数、明示的に使用する前に、その直接の使用を割り当てるシステムはそれをゼロのデフォルト値を代入し、ローカル変数のためにされていない場合あなたは明示的に割り当てる必要があり、または時間をコンパイルしないでください。
- 同時にについて
static
とfinal
修正の定数、明示的にそれ以外の場合は、時間をコンパイルしません、その宣言の時に割り当てる必要があります。定数の唯一の最後の変更は、あなたが明示的にも、声明の中で、それを割り当てることができます明示的にショートで、明示的にその使用前に割り当てておく必要があり、初期のケースに値を割り当てると、システムはそれらにゼロのデフォルト値を与えるものではありません。 - 参照データ型のために
reference
、等参照、オブジェクト参照、のような配列は、明示的システム、すなわち、デフォルト設定されますゼロ値を付与し、その直接の使用を割り当てるとしませんnull
。 - 値がアレイの初期化中にアレイ内の各要素に割り当てられていない場合、前記要素は、対応するデータの種類に応じて、ゼロのデフォルト値が与えられます。
- 最終の静的修飾しながら一定値プロパティクラスフィールド属性テーブル現在フィールド、すなわち場合は、属性ConstValue準備段階変数値で指定された値に初期化されます。上記のようにクラス変数の値が定義されていると仮定:
public static final int value = 3;
あるJavac一定値属性値は、コンパイル時に生成され、仮想マシンに応じて準備フェーズが一定値の値を提供する時に3」。我々として理解することができstatic final
、コンパイル時に定数は、そのクラスを呼び出すために一定のプールになります
分析:クラスが直接参照シンボル参照に変換され、
解決フェーズは、仮想マシンプロセスの定数プールは、主に分析動作のために、直接参照シンボリック参照を置換されている类
、または接口
、字段
、类方法
、接口方法
、方法类型
、方法句柄
および调用点
記号参照7クラスの修飾子。シンボリック参照は、任意のリテラルであることができるターゲットを記述するためのシンボルの集合です。
直接引用
これは、オブジェクトへの直接ポインタ、測位対象ハンドルへの間接的または相対オフセットです。
初期化
初期化、静的変数のクラスのための正しい初期値を与え、メインクラス変数を初期化するための責任JVMクラスが初期化されます。二つの方法でJavaクラスの変数に設定された初期値:
- クラス変数に指定された初期値を宣言します
- 静的コードブロックを使用すると、クラス変数の初期値が代入され
JVMの初期化ステップ
- このクラスは、ロードと接続し、プログラムに接続されていない場合は、クラスをロードします
- 直接の親クラスが初期化されていない場合は、最初にその直接の親を初期化
- クラス初期設定ステートメント場合、システム順次初期設定ステートメントを実行します
クラスの初期化タイミング:クラスの初期化の積極的な使用は、クラスをリードするときにのみ、積極的に使用するクラスは、次の6つのものがあります。
- クラスのインスタンスを作成し、新しい方法であります
- アクセス静的クラスまたはインタフェースの変数、または静的変数の代入
- 静的メソッド呼び出しクラス
- リフレクタ(例えばClass.forNameの( "com.pdai.jvm.Test"))
- クラスのサブクラスを初期化し、親クラスが初期化されます
- Java仮想マシンは、マスタークラス、直接のjava.exeコマンドを実行するために、クラスのクラス(Javaのテスト)を開始するためにマークされ始めたとき
使用
データ構造クラスの界面領域、オブジェクト領域データヒープにアクセスする方法。
アンインストール
Java仮想マシンは、生活状況のサイクルを終了します
- でSystem.exit()メソッドの実装
- 通常のプログラム実行が終了
- 中止され、実行中に例外やエラーが発生しました
- オペレーティング・システム・エラーがJava仮想マシンを終了させるプロセスを引き起こしたので
クラスローダ、JVMクラスローディング機構
クラスローダの階層
注:これは、代わりの組み合わせを使用するのでは、達成するために、継承を介して親クラスローダではありません。
Java仮想マシンが関係しているの視点に立ち、2つだけ異なるクラスローダがあります。クラスローダを起動します。それは、C ++の実装を使用しています(ここでは唯一
Hotspot
、それはJDK1.5後、デフォルトの仮想マシンであり、他の多くの仮想がありますマシンは、Java言語で実装されている)、仮想マシン自体の一部であり、他のすべてのクラスローダ:これらのクラスローダJava言語、仮想マシンの外部の独立、およびすべての抽象クラスから派生することによりjava.lang.ClassLoader
、クラスローダ後に他のクラスにロードするために、ブートクラスローダによってメモリにロードする必要があります。
Java開発者の透視図を立ち、クラスローダは、大きく以下の3つのカテゴリに分類することができます。
启动类加载器
:ブートストラップクラスローダは、JDK \ \の下に(JDK JDKのインストールディレクトリの代わりに、以下同じ)、JREのlibに格納されているロードする責任がある、または-Xbootclasspathパラメータには、仮想マシンのパスを指定すると、そのような室温と(ライブラリによって識別することができます。瓶、すべてのJava。*クラス)は、ブートストラップクラスローダの負荷を始めています。スタートクラスローダは、直接Javaプログラムを参照されていません。
扩展类加载器
:拡張クラスローダによるローダーsun.misc.Launcher$ExtClassLoader
それはJDK \ jre \ lib \ extディレクトリまたはシステム変数PATHで指定されたすべてのjava.ext.dirsライブラリをロードするための責任があるという認識、(例えばjavaxの*クラスの始まりとして。)開発者が直接拡張クラスローダーを使用することができます。
应用程序类加载器
:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
- 在执行非置信代码之前,自动验证数字签名。
- 动态地创建符合用户特定需要的定制化构建类。
- 从特定的场所取得java class,例如数据库中和网络中。
寻找类加载器
寻找类加载器小例子如下:
package com.pdai.jvm.classloader;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
结果如下:
sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null
从上面的结果可以看出,并没有获取到ExtClassLoader
的父Loader,原因是BootstrapLoader
(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null
。
类的加载
类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
package com.pdai.jvm.classloader;
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("Test2");
//使用Class.forName()来加载类,默认会执行初始化块
// Class.forName("Test2");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
// Class.forName("Test2", false, loader);
}
}
public class Test2 {
static {
System.out.println("静态初始化块执行了!");
}
}
分别切换加载方式,会有不同的输出结果。
Class.forName()和ClassLoader.loadClass()区别?
- Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
- ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
- Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
JVM类加载机制
全盘负责
,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入父类委托
,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类缓存机制
,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效双亲委派机制
, 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
双亲委派代码实现
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
双亲委派优势
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行
自定义类加载器
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:
package com.pdai.jvm.classloader;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("D:\\temp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.pdai.jvm.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。
这里有几点需要注意:
1、这里传递的文件名需要是类的全限定性名称,即com.pdai.jvm.classloader.Test2
格式的,因为 defineClass 方法是按这种格式进行处理的。
2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
3、这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把com/pdai/jvm/classloader/Test2.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
参考文章
http://www.cnblogs.com/ityouknow/p/5603287.html
http://blog.csdn.net/ns_code/article/details/17881581
https://segmentfault.com/a/1190000005608960
http://www.importnew.com/18548.html
http://zyjustin9.iteye.com/blog/2092131
http://www.codeceo.com/article/java-class-loader-learn.html
最全的Java后端知识体系 https://www.pdai.tech, 每天更新中...。