天天看點

Java類加載與初始化

在了解Java的機制之前,需要先了解類在JVM(Java虛拟機)中是如何加載的,這對後面了解java其它機制将有重要作用。

每個類編譯後産生一個Class對象,存儲在.class檔案中,JVM使用類加載器(Class Loader)來加載類的位元組碼檔案(.class),類加載器實質上是一條類加載器鍊,一般的,我們隻會用到一個原生的類加載器,它隻加載Java API等可信類,通常隻是在本地磁盤中加載,這些類一般就夠我們使用了。如果我們需要從遠端網絡或資料庫中下載下傳.class位元組碼檔案,那就需要我們來挂載額外的類加載器。

一般來說,類加載器是按照樹形的層次結構組織的,每個加載器都有一個父類加載器。另外,每個類加載器都支援代理模式,即可以自己完成Java類的加載工作,也可以代理給其它類加載器。

類加載器的加載順序有兩種,一種是父類優先政策,一種是是自己優先政策,父類優先政策是比較一般的情況(如JDK采用的就是這種方式),在這種政策下,類在加載某個Java類之前,會嘗試代理給其父類加載器,隻有當父類加載器找不到時,才嘗試自己去加載。自己優先的政策與父類優先相反,它會首先嘗試子經濟加載,找不到的時候才要父類加載器去加載,這種在web容器(如tomcat)中比較常見。

不管使用什麼樣的類加載器,類,都是在第一次被用到時,動态加載到JVM的。這句話有兩層含義:

Java程式在運作時并不一定被完整加載,隻有當發現該類還沒有加載時,才去本地或遠端查找類的.class檔案并驗證和加載;

當程式建立了第一個對類的靜态成員的引用(如類的靜态變量、靜态方法、構造方法——構造方法也是靜态的)時,才會加載該類。Java的這個特性叫做:動态加載。

需要區分加載和初始化的差別,加載了一個類的.class檔案,不以為着該Class對象被初始化,事實上,一個類的初始化包括3個步驟:

加載(Loading),由類加載器執行,查找位元組碼,并建立一個Class對象(隻是建立);

連結(Linking),驗證位元組碼,為靜态域配置設定存儲空間(隻是配置設定,并不初始化該存儲空間),解析該類建立所需要的對其它類的應用;

初始化(Initialization),首先執行靜态初始化塊static{},初始化靜态變量,執行靜态方法(如構造方法)。

Java在加載了類之後,需要進行連結的步驟,連結簡單地說,就是将已經加載的java二進制代碼組合到JVM運作狀态中去。它包括3個步驟:

驗證(Verification),驗證是保證二進制位元組碼在結構上的正确性,具體來說,工作包括檢測類型正确性,接入屬性正确性(public、private),檢查final class 沒有被繼承,檢查靜态變量的正确性等。

準備(Preparation),準備階段主要是建立靜态域,配置設定空間,給這些域設預設值,需要注意的是兩點:一個是在準備階段不會執行任何代碼,僅僅是設定預設值,二個是這些預設值是這樣配置設定的,原生類型全部設為0,如:float:0f,int 0, long 0L, boolean:0(布爾類型也是0),其它引用類型為null。

解析(Resolution),解析的過程就是對類中的接口、類、方法、變量的符号引用進行解析并定位,解析成直接引用(符号引用就是編碼是用字元串表示某個變量、接口的位置,直接引用就是根據符号引用翻譯出來的位址),并保證這些類被正确的找到。解析的過程可能導緻其它的類被加載。需要注意的是,根據不同的解析政策,這一步不一定是必須的,有些解析政策在解析時遞歸的把所有引用解析,這是early resolution,要求所有引用都必須存在;還有一種政策是late resolution,這也是Oracle 的JDK所采取的政策,即在類隻是被引用了,還沒有被真正用到時,并不進行解析,隻有當真正用到了,才去加載和解析這個類。

注 意:在《Java程式設計思想》中,說static{}子句是在類第一次加載時執行且執行一次(可能是筆誤或翻譯錯誤,因為此書的例子顯示static是在第 一次初始化時執行的),《Java深度曆險》中說 static{}是在第一次執行個體化時執行且執行一次,這兩種應該都是錯誤的,static{}是在第一次初始化時執行,且隻執行一次;用下面的代碼可以判 定出來:

可以看到,不執行個體化,隻執行forName初始化時,仍然會執行static{}子句,但不執行構造方法,是以輸出的隻有Initializing,沒有Building。

關于初始化,@阿春阿曉 在本文的評論中給出了很詳細的場景,感謝@阿春阿曉:

根據java虛拟機規範,所有java虛拟機實作必須在每個類或接口被java程式首次主動使用時才初始化。

主動使用有以下6種:

1) 建立類的執行個體

2) 通路某個類或者接口的靜态變量,或者對該靜态變量指派(如果通路靜态編譯時常量(即編譯時可以确定值的常量)不會導緻類的初始化)

3) 調用類的靜态方法

4) 反射(Class.forName(xxx.xxx.xxx))

5) 初始化一個類的子類(相當于對父類的主動使用),不過直接通過子類引用父類元素,不會引起子類的初始化(參見示例6)

6) Java虛拟機被标明為啟動類的類(包含main方法的)

類與接口的初始化不同,如果一個類被初始化,則其父類或父接口也會被初始化,但如果一個接口初始化,則不會引起其父接口的初始化。

1,通過上面的講解,将可以了解下面的程式(下面的程式部分來自于《Java程式設計思想》):

對上面的程式段,第一次調用Class.forName("Toy"),将執行static子句;如果在之後執行new Toy()都隻執行構造方法。

2,需要注意newInstance()方法

3,用類字面常量 .class和Class.forName都可以建立對類的應用,但是不同點在于,用Gum.class建立Class對象的應用時,不會自動初始化該Class對象(static子句不會執行)

使用Toy.class是在編譯期執行的,是以在編譯時必須已經有了Toy的.class檔案,不然會編譯失敗,這與 Class.forName("myblog.classloader.Toy")不同,後者是運作時動态加載。

但是,如果該main方法是直接寫在Toy類中,那麼調用Toy.class,會引起初始化,并輸出Initializing,原因并不是Toy.class引起的,而是該類中含有啟動方法main,該方法會導緻Toy的初始化。

 4,編譯時常量。回到完整的類Toy,如果直接輸出:System.out.println(Toy.price),會發現static子句和構造方法都沒有被執行,這是因為Toy中,常量price被static final限定,這樣的常量叫做編譯時常量,對于這種常量,不需要初始化就可以讀取。

編譯時常量必須滿足3個條件:static的,final的,常量。

下面幾種都不是編譯時常量,對它們的應用,都會引起類的初始化:

5,static塊的本質。注意下面的代碼:

這段代碼的輸出是什麼呢?Initialing在c、d、e之前輸出,還是在之後?e輸出的是5還是10?

執行一下,結果為:

答案是3最先輸出,Intializing随後輸出,e輸出的是10,為什麼呢?

原因是這樣的:輸出c時,由于c是編譯時常量,不會引起類初始化,是以直接輸出,輸出d時,d不是編譯時常量,是以會引起初始化操作,即static塊的執行,于是d被指派為5,e被指派為10,然後輸出Initializing,之後輸出d為5,e為10。

但e為什麼是10呢?原來,JDK會自動為e的初始化建立一個static塊(參考:http://www.java3z.com/cwbwebhome/article/article8/81101.html?id=2497),是以上面的代碼等價于:

可見,按順序執行,e先被初始化為5,再被初始化為10,于是輸出了10。

類似的,容易想到下面的代碼:

在這段代碼中,對e的聲明被放到static塊後面,于是,e會先被初始化為10,再被初始化為5,是以這段代碼中e會輸出為5。

6,當通路一個Java類或接口的靜态域時,隻有真正聲明這個域的類或接口才會被初始化(《Java深度曆險》)

在該例子中,雖然通過A來引用了value,但value是在父類B中聲明的,是以隻會初始化B,而不會引起A的初始化。