一、類的生命週期:
加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,但解析階段不一定,在某些情況下可以在初始化之後在開始,這是爲了支持java語言的運行時綁定(也稱動態綁定和晚期綁定)。關於運行時綁定可以查看這篇文章:http://zhangjunhd.blog.51cto.com/113473/49452/。
二、類加載的時機
什麼情況下開始類加載階段的第一個階段:加載?java虛擬機並沒有進行強制約束,這點交給虛擬機的具體實現來自由把握。
但是對於初始化階段,虛擬機規範則是嚴格規定了有且只有5種情況必須立即對類進行初始化(主動使用)。
1、使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用類的靜態方法的時候。
2、使用java.lang.reflect包的方法對類進行反射調用的時候(如Class.forName("con.xx.Test"))。
3、初始化一個類的子類的時候。
4、虛擬機執行啓動時,被標明爲啓動類的類(java Test)。
5、使用jdk1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。(關於jdk1.7的動態語言——http://www.infoq.com/cn/articles/jdk-dynamically-typed-language/)
除主動引用(上述5中場景)之外,其他所有引用類的方式都不會觸發初始化,這些其他類的引用方式稱爲被動使用。
比如:
1、通過子類引用父類的靜態字段,不會導致子類初始化。
2、通過數組定義來引用類,不會觸發此類的初始化。
3、常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發。
4、通過類名獲取Class對象,不會觸發類的初始化。
5、通過Class.forName加載指定類時,如果指定參數initialize(這個參數告訴虛擬機,要不要對類進行初始化)爲false時,也不會觸發類的初始化。
6、通過ClassLoader默認的loadClass方法,也不會觸發初始化。
三、類的生命週期的各個階段各發生了什麼
加載
加載時類加載的第一個過程,在這個階段,將完成一下三件事情:
1. 通過一個類的全限定名獲取該類的二進制流。
2. 將該二進制流中的靜態存儲結構轉化爲方法去運行時數據結構。
3. 在內存中生成該類的Class對象,作爲方法區該類的數據訪問入口。
驗證
驗證的目的是爲了確保Class文件的字節流中的信息不會危害到虛擬機.在該階段主要完成以下四鍾驗證:
1. 文件格式驗證:驗證字節流是否符合Class文件的規範,如是否以魔數0xCAFEBABE開頭,主、次版本號是否在當前虛擬機處理範圍內,常量池中的常量是否有不被支持的類型等等。
2. 元數據驗證:對字節碼描述的信息進行語義分析,如這個類是否有父類,是否繼承了不允許被繼承的類(被final修飾的類)等。
3. 字節碼驗證:是整個驗證過程中最複雜的一個階段,通過驗證數據流和控制流的分析,確定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉指令是否正確等。
4. 符號引用驗證:這個動作在後面的解析過程中發生,主要是爲了確保解析動作能正確執行。對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗,通常驗證:符號引用中通過字符串描述的全限定名能否找到對應的類、在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段等。
準備
準備階段是爲類的靜態變量分配內存並將其初始化爲默認值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。
public static int value=123;//在準備階段value初始值爲0 。在初始化階段纔會變爲123 。
但是注意如果聲明爲:
public static final int value = 123 //在編譯階段會爲v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將value賦值爲123
解析
該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之前,也有可能在初始化之後。
初始化
初始化時類加載的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。
初始化階段是執行類構造器<clinit>()方法的過程。關於<clinit>()方法:
1、<clinit>()方法是由編譯器自動收集類中所有的類變量的賦值動作和靜態代碼塊(static{}塊)中的語句合併產生的。收集的順序由語句在源文件中出現的順序決定,靜態代碼塊只能f訪問定義在它之前的變量,定義在它之後的變量可以賦值卻不能訪問(編譯報錯:Illegal forward reference)。
2、父類的<clinit>()方法先執行,也就意味着父類中的靜態代碼塊優先於子類的變量賦值。
3、<clinit>()方法對於類或接口來說不是必須的。即一個類要是沒有變量賦值,也沒有靜態代碼塊,編譯器就可以不爲其生成<clinit>()方法。
4、接口中不能使用靜態代碼塊,但仍然有變量初始化的賦值操作,因此接口也會生成<clinit>()方法,不同的是執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法,只有當父接口中定義變量被使用時,它纔會被初始化,另外接口的實現類在初始化時也不會初始化接口。
5、虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。
四、類加載器及雙親委派模型
jvm提供了3種類加載器:
1、啓動類加載器(Bootstrap ClassLoader):負責加載< JAVA_HOME>\lib目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機識別(按文件名識別,如rt.jar)的類。
2、擴展類加載器(Extension ClassLoader):這個類加載器由sun.misc.Launcher$ExtClassLoader實現,負責加載
<JAVA_HOME>\lib\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。
3、應用程序類加載器(Application ClassLoader):在個類加載器由sun.misc.Launcher$AppClassLoader實現,由於他是ClassLoader中的getSystemClassLoader()方法的返回值,所以也稱爲系統類加載器,負責加載用戶路徑(classpath)上的類庫。
雙親委派模型如下:
雙親委派模型的工作過程:一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個加載請求委派給父類加載器去完成,每一層的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。
需要指出的是,加載器之間的父子關係實際上去組合關係而不是繼承關係
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機中的唯一性,每一個類加載器 ,都擁有一個獨立的命名空間(子加載器的命名空間包含所有父加載器的命名空間。因此由子加載器加載的類能看到父加載器看到的類,而父加載器加載的類看不到子加載器加載的類)。
比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載他們的
類加載器不同,那這兩個類就必定不相等(equals,isAssignableFrom,isInstance等方法)。
五、如何實現自定義類加載器
先看看ClassLoader類的loadClass()方法
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) {//找加載器的父加載器。如果父加載器不是null(不是Bootstrap ClassLoader),那麼就執行父加載器的loadClass方法
c = parent.loadClass(name, false);
} else {//把類加載請求一直向上拋,直到父加載器爲null(是Bootstrap ClassLoader)爲止
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);
// 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;
}
}
再看一下findClass()這個方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
因此1、如果不想打破雙親委派模型,那麼只需要重寫findClass方法即可
2、如果想打破雙親委派模型,那麼就重寫整個loadClass方法
當然,我們自定義的ClassLoader不想打破雙親委派模型,所以自定義的ClassLoader繼承自java.lang.ClassLoader並且只重寫findClass方法。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
private String name; //類加載器的名字
private String path = "d:\\"; //加載類的路徑
private final String fileType = ".class"; //class文件的擴展名
public MyClassLoader(String name){
super(); //讓系統類加載器成爲該類加載器的父加載器
this.name = name;
}
public MyClassLoader(ClassLoader parent ,String name){
super(parent); //顯示指定該類加載器的父加載器
this.name = name;
}
@Override
public String toString() {
return this.name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return this.defineClass(name,data,0,data.length);
}
private byte[] loadClassData(String name){
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try{
this.name = this.name.replace(".","\\"); //com.xx.xx 換成com\xx\xx
is = new FileInputStream(new File(path + name +fileType));
baos = new ByteArrayOutputStream();
int ch = 0;
while(-1 != (ch = is.read())){
baos.write(ch);
}
data = baos.toByteArray();
}catch (Exception e){
e.printStackTrace();
}finally {
try{
is.close();
baos.close();
}catch (Exception e){
e.printStackTrace();
}
}
return data;
}
public static void main(String[] args) throws Exception{
MyClassLoader loader1 = new MyClassLoader("loader1"); //父加載器爲系統類加載器
loader1.setPath("d:\\myapp\\serverlib\\");
MyClassLoader loader2 = new MyClassLoader(loader1,"loader2");
loader2.setPath("d:\\myapp\\clientlib\\");
MyClassLoader loader3 = new MyClassLoader(null,"loader3");//null表示父加載器是根類加載器
loader3.setPath("d:\\myapp\\otherlib\\");
test(loader2);
test(loader3);
}
public static void test(ClassLoader loader) throws Exception{
Class clazz = loader.loadClass("Sample");
Object object = clazz.newInstance();
}
}
自定義類加載器可以拿來做什麼呢:
1、加密:java代碼很容易被反編譯,如果你需要把自己的代碼進行加密,可以先將編譯後的代碼用某種加密算法加密,然後實現自己的類加載器,負責將這段加密後的代碼還原。
2、從非標準的來源加載代碼:例如你的部分字節碼是放在數據庫中甚至是網絡上的,就可以自己寫個類加載器,從指定的來源加載類。
3、動態創建:爲了性能等等可能的理由,根據實際情況動態創建代碼並執行。
4、javaWeb服務器,如tomcat都實現了自己的不同的類加載器,使得類庫能被不同的應用程序隔離或共享等等。