7.1 概述
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。數組
類是在運行期間第一次使用時動態加載的,而不是編譯時期一次性加載。安全
7.2 類加載的時機
類的生命週期
![](http://static.javashuo.com/static/loading.gif)
類的生命週期:網絡
- 加載
- 驗證
- 準備
- 解析
- 初始化
- 使用
- 卸載
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。數據結構
5種狀況須要「初始化」
對於初始化階段,虛擬機規範則嚴格規定了有且只有5種狀況必須當即對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):多線程
- 遇到new、 getstatic、 putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。 生成這4條指令的最多見的Java代碼場景是:使用new關鍵字實例化對象的時候、 讀取或設置一個類的靜態字段(被final修飾、 已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
- 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
- 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
- 當使用JDK 1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果爲REF_getStatic、 REF_putStatic、 REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。
如下幾種常見的狀況,不會觸發初始化:函數
- 經過子類引用父類的靜態字段,不會致使子類初始化。
- 經過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自Object的子類,其中包含了數組的屬性和方法。
- 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。
7.3 類加載的過程
包含了加載、驗證、準備、解析和初始化這 5 個階段。佈局
1.加載
完成如下3件事:spa
- 經過一個類的全限定名來獲取定義此類的二進制字節流。
- 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
- 在內存中生成一個表明這個類的 Class 對象,做爲方法區這個類的各類數據的訪問入口。
其中二進制字節流能夠從如下方式中獲取:線程
- 從 ZIP 包讀取,成爲 JAR、EAR、WAR 格式的基礎。
- 從網絡中獲取,最典型的應用是 Applet。
- 運行時計算生成,例如動態代理技術,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理類的二進制字節流。
- 由其餘文件生成,例如由 JSP 文件生成對應的 Class 類。
2.驗證
確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體上會完成下面4個階段的檢驗動做:
- 文件格式驗證
- 元數據驗證
- 字節碼驗證
- 符號引用驗證
3.準備
準備階段是正式爲類變量(被static修飾的變量)分配內存並設置類變量初始值(「一般狀況」下是數據類型的零值)的階段,這些變量所使用的內存都將在方法區中進行分配。
注:實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。
4.解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
- 符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量(明肯定義在Java虛擬機規範的Class文件格式中)。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。
- 直接引用(Direct References):直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,若是有了直接引用,那引用的目標一定已經在內存中存在。
5.初始化
到了初始化階段,才真正開始執行類中定義的Java程序代碼(或者說是字節碼)。初始化階段是執行類構造器< clinit >()方法的過程。
- < clinit >()方法是由編譯器自動收集類中的所有類變量的賦值動做和靜態語句塊(static{}塊)中的語句合併產生的。編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。
public class Test{
static{
i=0; //給變量賦值能夠正常編譯經過
System.out.print(i); //這句編譯器會提示"非法向前引用"
}
static int i=1;
}
- < clinit >()方法與類的構造函數(或者說實例構造器< init >()方法)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的< clinit >()方法執行以前,父類的< clinit >()方法已經執行完畢。所以在虛擬機中第一個被執行的< clinit >()方法的類確定是java.lang.Object。
- 父類的< clinit >()方法先執行,意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。
static class Parent{
public static int A=1;
static{
A=2;
}
}
static class Sub extends Parent{
public static int B=A;
}
public static void main(String[] args){
System.out.println(Sub.B); //2
}
- < clinit >()方法對於類或接口不是必須的,若是一個類中不包含靜態語句塊,也沒有對類變量的賦值操做,編譯器能夠不爲該類生成< clinit >()方法。
- 接口中不可使用靜態語句塊,但仍然有類變量初始化的賦值操做,所以接口與類同樣都會生成< clinit >()方法。但與類不一樣的是,執行接口的< clinit >()方法不須要先執行父接口的< clinit >()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的< clinit >()方法。
- 虛擬機會保證一個類的< clinit >()方法在多線程環境下被正確的加鎖和同步,若是多個線程同時初始化一個類,只會有一個線程執行這個類的< clinit >()方法,其它線程都會阻塞等待,直到活動線程執行< clinit >()方法完畢。若是在一個類的< clinit >()方法中有耗時的操做,就可能形成多個線程阻塞,在實際過程當中此種阻塞很隱蔽。
7.4 類加載器
類與類加載器
對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。
這裏的「相等」,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果爲 true,也包括使用 instanceof 關鍵字作對象所屬關係斷定結果爲 true。
類加載器分類
從 Java 虛擬機的角度來說,只存在如下兩種不一樣的類加載器:
- 啓動類加載器(Bootstrap ClassLoader),這個類加載器用 C++ 實現,是虛擬機自身的一部分;
- 全部其餘類的加載器,這些類由 Java 實現,獨立於虛擬機外部,而且全都繼承自抽象類 java.lang.ClassLoader。
從 Java 開發人員的角度看,類加載器能夠劃分得更細緻一些:
- 啓動類加載器(Bootstrap ClassLoader):此類加載器負責將存放在
\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即便放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被 Java 程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給啓動類加載器,直接使用 null 代替便可。
- 擴展類加載器(Extension ClassLoader):這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將
/lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的全部類庫加載到內存中,開發者能夠直接使用擴展類加載器。
- 應用程序類加載器(Application ClassLoader):這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。因爲這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以通常稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
雙親委派模型
應用程序都是由以上3種類加載器互相配合進行加載的,若是有必要,還能夠加入
本身定義的類加載器。
下圖展現的類加載器之間的這種層次關係,稱爲類加載器的雙親委派模型(Parents
Delegation Model,JDK1.2以後才被引入)。該模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。這裏類加載器之間的父子關係通常不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼。
![](http://static.javashuo.com/static/loading.gif)
一個類加載器首先把類加載請求委派給父類加載器去完成,只有當父加載器沒法完成時,子加載器纔會嘗試本身去加載。
Java 類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。保證Java程序的穩定運做。
實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中。邏輯是:先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器做爲父加載器。若是父類加載失敗,拋出ClassNotFoundException異常後,再調用本身的findClass()方法進行加載。