1. 程式人生 > >java class檔案的載入

java class檔案的載入

java class檔案載入過程:jvm把描述的資料從class檔案載入(loading)到記憶體(java方法區)中,中間對資料進行校驗(verification)、轉換解析(resolution)和初始化(initialization),最終形成可以被jvm直接使用的Java類,這就是class檔案的載入。

例如:Student.class,通過class檔案的載入,就可以直接通過newInstance建立物件,供jvm使用。

同時jvm把class檔案載入到記憶體中,在jvm中就形成一份描述Class結構的元資訊物件(Class物件,存在java堆中),通過該元資訊物件就可以獲知Class的結構資訊,例如:建構函式、屬性、方法等,Java也允許使用者藉由這個元資訊物件間接呼叫Class物件的功能。

例如:
1 Class.forName(“classLoader.ClassLoaderTest”).getClassName();//獲取class的名稱
2 Class.forName(“classLoader.ClassLoaderTest”).getClassLoader();//獲取類載入器
3 Class.forName(“classLoader.ClassLoaderTest”).getMethod();//獲取方法
4 User.class.getClassLoader().loadClass(“classLoader.ClassLoaderTest”).getField();//獲取屬性

class的生命週期

![class的生命週期](https://img-blog.csdn.net/20171024145920697?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDY1MjU3Ng==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

類載入的過程包括了載入、驗證、準備、解析、初始化五個階段。在這五個階段中,載入、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援Java語言的執行時繫結(也成為動態繫結或晚期繫結)。另外注意這裡的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中呼叫或啟用另一個階段,這就有可能出現物件雖然不為null,但是仍存在部分欄位沒有初始化完全,因此單利模式的double-check-lock的物件需要加volatile修飾。

載入(loading)

主要做了3件事:
1 通過一個類的全限定名來獲取其定義的二進位制位元組流。就是常見的Class.forName(className)中的className,一般都是包名類名。
2 把該位元組流所代表的靜態儲存結構轉化為方法取的執行資料結構,此時相關的class類的相關資訊就儲存到方法區。
3 根據class檔案,在java堆中建立一個該class檔案對應的Class物件,作為方法取中資料的訪問入口。
相對於類載入的其他階段而言,載入階段(準確地說,是載入階段獲取類的二進位制位元組流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類載入器來完成載入,也可以自定義自己的類載入器來完成載入,主要是通過自定義類載入器進行控制。
關於類載入器如何查詢到class檔案以及如何載入,請參考:

連線(linking)

連線分為以下幾步:驗證、準備、解析。

驗證:驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
準備:為類的靜態變數分配記憶體,並將其初始化為預設值。,注意僅對靜態變數進行記憶體分配和初始化值,這個值是系統預設的初始化值,例如:0,0L,”“,null等,而不是程式碼中賦的值。
示例:public static int value = 3;
此時 value的值就是系統預設0,而不是程式碼中賦的值3,3需要到初始化的時候進行復制。當然如果被修飾為final static int value = 3,那麼準備結束後,value就是3了,static final常量在編譯期就將其結果放入了呼叫它的類的常量池中。
還需要注意如下幾點:
1 對基本資料型別來說,對於類變數(static)和全域性變數,如果不顯式地對其賦值而直接使用,則系統會為其賦予預設的零值,而對於區域性變數來說,在使用前必須顯式地為其賦值,否則編譯時不通過。例如:方法內定義 int i;會提示initialize variable 初始化變數。
2 對於同時被static和final修飾的常量,必須在宣告的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在宣告時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予預設零值。
3 對於引用資料型別reference來說,如陣列引用、物件引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予預設的零值,即null。
4 如果在陣列初始化時沒有對陣列中的各元素賦值,那麼其中的元素將根據對應的資料型別而被賦予預設的零值。
解析:把類中的符號引用轉換為直接引用。解析階段是虛擬機器將常量池內符號引用替換為直接引用的過程,解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制代碼。

初始化(initialization)

為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化。例如:static int a = 3;解析後a的值為0,初始化之後a的值為3。

初始化的方式主要有2種:
1 指定初始值,例如:static int a = 3;直接宣告。
2 通過靜態程式碼塊,進行賦值。
例如:static int a;static{ a =3;},初始化之後a的值為3。相當於static程式碼塊就是給static變數初始化值使用,並且只執行一次。

jvm的初始化步驟:
   1 該類是否被載入過,如果沒有,就進行loading,linking後,再初始化。
   2 如果該類的直接父類沒有進行初始化,就先初始化直接父類。
   3 如果類中有初始化語句(static程式碼塊,或者賦值語句),一次執行初始化語句。
   記住:優先初始化該類的直接父類。
jvm初始化的時機:
    1 直接建立類的例項,通過new的方式,這是會類初始化,當然除了初始化,還有後續的例項化。
    2 訪問某個類\介面的靜態變數(get/set值),此時會直接初始化該靜態變數所在的類。注意:即使通過子類訪問父類的靜態變數,那麼也只會初始化父類。      
package classLoader.dynamic;

public class Parent {
    static String name;
    static{
        System.out.println("parent init--befor:"+name);
        name="123";
        System.out.println("parent init--after:"+name);
    }
    public static void main(String[] args) {
        System.out.println("print:"+Son.name);
    }
}

class Son extends Parent{
    static{
        System.out.println("son init");
    }

}

輸出結果-------
parent init--befor:null
parent init--after:123
print:123

並沒有執行Son中的static程式碼塊,同時需要注意:main方法在Parent中,如果main方法在Son中,Son的static就會執行,這是因為Son是main方法的入口,需要初始化:輸出如下:
parent init--befor:null
parent init--after:123
son init
print:123

3 呼叫類的靜態方法。
4 反射(如Class.forName(“com.shengsiyuan.Test”))。
5 初始化某個類的子類,則其父類也會被初始化,這是隱式的初始化。
6 Java虛擬機器啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來執行某個主類。

類載入器

把class檔案,載入進jvm記憶體中,需要通過類載入器,jvm的類載入器分為4種(從載入內容的位置分):

1 啟動類載入器(bootstrap classloader):jvm核心的類載入器,是虛擬機器自身的類載入器,無法被Java程式直接引用。負責載入%JAVA_HOME%\jre\lib下的所有jar包,例如:String類的核心jar包,就是有bootstrap classLoader載入的。
2 擴充套件類載入器(extention classLoader):繼承自ClassLoader物件,需要由bootstrap classLoader載入後,才能載入其他類,同時該類的父親就是bootstrap classLoader,負責載入:%JAVA_HOME%\jre\lib\ext可以被Java程式呼叫,用來載入其他class,注意:兩者的父子關係並不是通過繼承實現的,而是通過組合實現的,即通過類中的parent屬性獲取父類。
3 自定義類載入器(custom classLoader):如果上述類載入器不滿足需要,可以自定義classLoader,從指定的位置載入class,同時可以進行一些載入前和載入後的處理,例如:載入前進行解密、不進行class檔案的快取等。自定義類載入器需要繼承自ClassLoader,或者繼承自一些系統提供給的classLoader,例如:URLClassLoader,只需重寫裡面的findClass方法即可。

一般自定義classLoader有如下應用:
    (1)在執行非置信程式碼之前,自動驗證數字簽名,比如:那麼為了安全需要對class檔案加密,那麼就可以自定義classLoader進行解密和轉化。
    (2) 動態地建立符合使用者特定需要的定製化構建類。這個主要是用來動態載入class檔案,保證檔案的修改可以立刻生效。
    (3) 從特定的場所取得java class,例如資料庫中和網路中。

JVM類載入機制

1 全盤負責制:當一個類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入。例如:application classLoader載入User class檔案,那麼其中User通過繼承的父類、實現的介面類、匯入的jar包等,載入都是有按品牌裡餐廳 classLoader負責。
2 父類委託:當前載入器會先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類,這就是類載入的雙親委派。通過父類委託,當appliation載入User class檔案時,會優先詢問父類是否載入,如果父類沒有載入,那麼application classLoader就會嘗試在classPath路徑下載入改類,這同樣適用於User繼承的父類、實現的介面類等。

類載入機制

1 命令列啟動應用時候由JVM初始化載入。
2 通過Class.forName()方法動態載入,除了載入進記憶體,同時會進行初始化。
3 通過ClassLoader.loadClass()方法動態載入,這種載入只是把class檔案載入進記憶體,並不進行初始化。

示例:

package classLoader;

public class ClassInit {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("classLoader.Son");//方法一
        ClassLoader.getSystemClassLoader().loadClass("classLoader.Son");//方法二
    }
}

class Son{
    static String name;
    static{
        System.out.println("init-before:"+name);
        name="123";
        System.out.println("init-after:"+name);
    }
}



-------輸出結果:
方法一:
    init-before:null
    init-after:123
方法二:

方法一與方法二是互斥的。其中方法二並不會初始化,因此沒有輸出。
當然Class.forName(),也可以通過引數配置,不進行初始化。

雙親委派模型

雙親委派模型的工作流程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。

示例:
1 當用戶載入一個class檔案,獲取系統的class loader,預設的是Application classLoader,此時Application classLoader先去快取中,檢視該class是否被jvm記錄了,有記錄就返回,如果沒有就請求Extention calssLoader,如果extention classLoader沒有載入成功,那麼Application classLoader就會去classpath路徑下載入,仍為載入成功丟擲ClassNotFindException。
2 Extention classLoader 也不會直接載入class檔案,而是交給父載入器bootstrap classLoader去載入,如果bootstrap classLoader沒有載入成功,那麼extention classLoader就會去%JAVA_HOME%\jre\lib\ext下面查詢,成功就載入,否則就返回null,交給Application classLoader載入。
3 因為bootstrap classLoader載入器沒有父載入器,因此bootstrap classLoader直接在%JAVA_HOME%\jre\lib下,檢索該class檔案,如果沒有就返回null,交給extention classLoader去載入。

    雙親委派模型意義:
    -系統類防止記憶體中出現多份同樣的位元組碼
    -保證Java程式安全穩定執行

ClassLoader.java的loadClass原始碼

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;
        }

自定義類載入器

通常我們使用系統的類載入器(預設為Application classLoader)。但是,有的時候,我們也需要自定義類載入器。比如應用是通過網路來傳輸 Java 類的位元組碼,為保證安全性,這些位元組碼經過了加密處理,這時系統類載入器就無法對其進行載入,這樣則需要自定義類載入器來實現。自定義類載入器一般都是繼承自 ClassLoader 類,從上面對 loadClass 方法來分析來看,我們只需要重寫 findClass 方法即可。為什麼只需要重寫findClass方法即可?

1 首先從ClassLoader的構造方法中看:

//無參建構函式:呼叫了有參建構函式,其中getSystemClassLoader()方法,獲取系統的classLoader作為ClassLoader的parent,而通過ClassLoader.getSystemClassLoader()方法,獲取的類載入器就是Application classLoader,因此不需要考慮雙親委派載入模型了。

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
//有參建構函式,指定classLoader作為ClassLoader的父載入器。
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains =
            Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

2 從ClassLoader的loadClass方法中,發現當Bootstrap classLoader、ExtentionClassLoader、Application classLoader、ClassLoader都為成功載入class檔案後,會丟擲ClassNotFindException,此時catch後,呼叫了findClass,因此我們只需要重新findClass,把我們的class檔案返回即可。

從ClassLoader提供的範例看

//繼承ClassLoader
 class NetworkClassLoader extends ClassLoader {
    String host;
    int port;
    //重寫findCladd
    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    //載入Class檔案,生成位元組陣列即可,因此可以在loadClassData()方法中,新增自定義方法即可,例如解密,class檔案的獲取等。
    private byte[] loadClassData(String name) {
        // load the class data from the connection
         . . .
    }
}

注意:
    1 如果是自定義classLoader,儘量不要重寫ClassLoader的loadClass方法,因為這會破壞雙親委派。
    2 如果是載入本地class,該class檔案不要放在classpath中,因為雙親委派,application classLoader會提前載入。
    3 findClass(String namee)方法的name儘量按照全路徑(包名+類名),因為defineClass方法是按照這種方式處理,同時全路徑也能解決class檔案的快取的唯一性。
說明:
    1 如果想更好的瞭解自定義classLoader,可以參考URLClassLoader,根據url地址,載入指定名稱的class檔案。
    2 class檔案載入進jvm中,判斷Class物件的唯一性,就依靠,loadClass(String name)和使用的類載入器,兩者全部相同時,才認為是同一個Class,例如:com.classLoader.A.class和com.classLoader.B.class就不是同一個Class物件;由Application classLoader載入的com.classLoader.A.class和Extention classLoader載入的com.classLoader.A.class同樣不是同一個Class物件。

參考文章:
1 http://www.cnblogs.com/ityouknow/p/5603287.html 文章主要是參考這篇文章寫的。
2 http://www.importnew.com/18548.html
3 http://blog.csdn.net/u013256816/article/details/50837863
2和3的參考文章,其中關於子父類的static程式碼塊、構造程式碼塊、程式碼塊的執行順序有很好的解說。