原文地址java
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,This
is the class loading mechanism of the virtual machine
本文基於HotSpot虛擬機程序員
類從被加載到虛擬機內存開始,到卸載出內存爲止,整個過程包括加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3部分統稱爲鏈接。數據庫
其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。數組
關於靜態綁定和動態綁定:緩存
靜態綁定(前期綁定)是指:在程序運行前就已經知道方法是屬於那個類的,在編譯的時候就能夠鏈接到類的中,定位到這個方法。安全
在Java中,final、private、static修飾的方法以及構造函數都是靜態綁定的,不需程序運行,不需具體的實例對象就能夠知道這個方法的具體內容。服務器
動態綁定(後期綁定)是指:在程序運行過程當中,根據具體的實例對象才能具體肯定是哪一個方法。網絡
動態綁定是多態性得以實現的重要因素,它經過方法表來實現:每一個類被加載到虛擬機時,在方法區保存元數據,其中,包括一個叫作 方法表(method table)的東西,表中記錄了這個類定義的方法的指針,每一個表項指向一個具體的方法代碼。若是這個類重寫了父類中的某個方法,則對應表項指向新的代碼實現處。從父類繼承來的方法位於子類定義的方法的前面。數據結構
加載是「類加載」過程的一個階段,這個階段須要完成如下3件事情:函數
關於獲取類的二進制字節流的方法,虛擬機並無指明要從哪裏獲取,如何獲取。
在Java的發展歷程中,主要出現瞭如下幾種方法
相對於類加載的其餘階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動做)是可控性最強的階段,由於開發人員既可使用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載。
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在Java堆中也建立一個java.lang.Class類的對象,這樣即可以經過該對象訪問方法區中的這些數據。
驗證階段的主要目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
危險因素的來源:
Java語言自己是安全的,可是因爲Class文件並不必定由Java源代碼編譯而來。因此極可能會載入有害的字節流而致使系統崩潰。
不一樣的虛擬機對類驗證的實現可能會有所不一樣,但大體都會完成如下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。
主要驗證字節流是否符合Class文件格式的規範,而且可以被當前版本的虛擬機處理。主要包括如下這些驗證點:
魔數的概念:不少類型的文件,其起始的幾個字節的內容是固定的(或是有意填充,或是本就如此)。根據這幾個字節的內容就能夠肯定文件類型,所以這幾個字節的內容被稱爲魔數 (magic number)。
這個階段的驗證是基於二進制字節流進行的,以後的3個驗證階段所有基於方法區的存儲結構進行的,不會在直接操做字節流。
對字節碼描述的信息進行語義分析(其實就是對類中的各數據類型進行語法校驗),以保證其描述的信息符合Java語言的規範要求。
該階段驗證的主要工做是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會作出危害虛擬機安全的行爲。
這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化爲直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身之外的信息(常量池中的各類符號引用)進行匹配性的校驗。一般要校驗如下內容:
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值
假設一個類變量的定義爲:
public static int value = 10;
那麼變量value在準備階段事後的初始值爲0,而不是10,由於這時候還沒有開始執行任何Java方法,而把value賦值爲3的putstatic指令是在程序編譯後,存放於類構造器<clinit>()
方法之中的,因此把value賦值爲10的動做將在初始化階段纔會執行。
基本數據類型的零值以下:
這一階段還須要注意以下幾點:
若是類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變量value就會被初始化爲ConstValue屬性所指定的值。
假設上面的類變量value被定義爲:
public static final int value = 10;
編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲10。
static final常量在編譯期就將其結果放入了調用它的類的常量池中。
例如:
public class Test { public static int value = 10; public Test() { System.out.println("This is Test Class"); } } public class Main { public static void main(String[] args) { System.out.println(Test.value); } }
以上代碼只會打印10,不會打印This is Test Class
解析階段是虛擬機將常量池中的符號引用轉化爲直接引用的過程。
對同一個符號引用進行屢次解析請求時很常見的事情,虛擬機實現可能會對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標示爲已解析狀態),從而避免解析動做重複進行。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
判斷所要轉化成的直接引用是對數組類型,仍是普通的對象類型的引用,從而進行不一樣的解析。
對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,若是有,則查找結束;若是沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,尚未,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束。
class Super{ public static int m = 11; static{ System.out.println("執行了super類靜態語句塊"); } } class Father extends Super{ public static int m = 33; static{ System.out.println("執行了父類靜態語句塊"); } } class Child extends Father{ static{ System.out.println("執行了子類靜態語句塊"); } } public class StaticTest{ public static void main(String[] args){ System.out.println(Child.m); } } 執行結果以下: 執行了super類靜態語句塊 執行了父類靜態語句塊 33 若是註釋掉Father類中對m定義的那一行,則輸出結果以下: 執行了super類靜態語句塊 11
static變量發生在靜態解析階段,也便是初始化以前,此時已經將字段的符號引用轉化爲了內存引用,也便將它與對應的類關聯在了一塊兒,因爲在子類中沒有查找到與m相匹配的字段,那麼m便不會與子類關聯在一塊兒,所以並不會觸發子類的初始化。
理論上是按照上述順序進行搜索解析,但在實際應用中,虛擬機的編譯器實現可能要比上述規範要求的更嚴格一些。若是有一個同名字段同時出如今該類的接口和父類中,或同時在本身或父類的接口中出現,編譯器可能會拒絕編譯。
對類方法的解析與對字段解析的搜索步驟差很少,只是多了判斷該方法所處的是類仍是接口的步驟,並且對類方法的匹配搜索,是先搜索父類,再搜索接口。
與類方法解析步驟相似,知識接口不會有父類,所以,只遞歸向上搜索父接口就好了。
初始化階段是類加載過程的最後一步,初始化階段是真正執行類中定義的Java程序代碼(或者說是字節碼)的過程。初始化過程是一個執行類構造器<clinit>()
方法的過程,根據程序員經過程序制定的主觀計劃去初始化類變量和其它資源。把這句話說白一點,其實初始化階段作的事就是給static變量賦予用戶指定的值以及執行靜態代碼塊。
Java虛擬機規範嚴格規定了有且只有5種場景必須當即對類進行初始化:
class Father{ public static int a = 1; static{ a = 2; } } class Child extends Father{ public static int b = a; } public class ClinitTest{ public static void main(String[] args){ System.out.println(Child.b); } }
執行上面的代碼,會打印出2,也就是說b的值被賦爲了2。
咱們來看獲得該結果的步驟。首先在準備階段爲類變量分配內存並設置類變量初始值,這樣A和B均被賦值爲默認值0,然後再在調用<clinit>()
方法時給他們賦予程序中指定的值。當咱們調用Child.b時,觸發Child的<clinit>()
方法,根據規則2,在此以前,要先執行完其父類Father的<clinit>()
方法,又根據規則1,在執行<clinit>()
方法時,須要按static語句或static變量賦值操做等在代碼中出現的順序來執行相關的static語句,所以當觸發執行Father的<clinit>()
方法時,會先將a賦值爲1,再執行static語句塊中語句,將a賦值爲2,然後再執行Child類的<clinit>()
方法,這樣便會將b的賦值爲2.
若是咱們顛倒一下Father類中「public static int a = 1;」語句和「static語句塊」的順序,程序執行後,則會打印出1。很明顯是根據規則1,執行Father的<clinit>()
方法時,根據順序先執行了static語句塊中的內容,後執行了「public static int a = 1;」語句。
另外,在顛倒兩者的順序以後,若是在static語句塊中對a進行訪問(好比將a賦給某個變量),在編譯時將會報錯,由於根據規則1,它只能對a進行賦值,而不能訪問。
類加載器雖然只用於實現類的加載動做,但它在Java程序中起到的做用卻遠遠不限於類的加載階段。對於任意一個類,都須要由它的類加載器和這個類自己一同肯定其在就Java虛擬機中的惟一性,也就是說,即便兩個類來源於同一個Class文件,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。這裏的「相等」包括了表明類的Class對象的equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用instanceof關鍵字對對象所屬關係的斷定結果。
從Java虛擬機的角度來說,只存在兩種不一樣的類加載器:
從Java開發人員的角度來看,類加載器能夠大體劃分爲如下三類:
應用程序都是由這三種類加載器互相配合進行加載的,若是有必要,咱們還能夠加入自定義的類加載器。由於JVM自帶的ClassLoader只是懂得從本地文件系統加載標準的java class文件,所以若是編寫了本身的ClassLoader,即可以作到以下幾點:
事實上當使用Applet的時候,就用到了特定的ClassLoader,由於這時須要從網絡上加載java class,而且要檢查相關的安全信息,應用服務器也大都使用了自定義的ClassLoader技術。
如上圖展現的類加載之間的這種層次關係,稱爲類加載器的雙親委派模型 咱們把每一層上面的類加載器叫作當前層類加載器的父加載器,固然,它們之間的父子關係並非經過繼承關係來實現的,而是使用組合關係來複用父加載器中的代碼。該模型在JDK1.2期間被引入並普遍應用於以後幾乎全部的Java程序中,但它並非一個強制性的約束模型,而是Java設計者們推薦給開發者的一種類的加載器實現方式。
雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。
使用雙親委派模型來組織類加載器之間的關係,有一個很明顯的好處,就是Java類隨着它的類加載器(說白了,就是它所在的目錄)一塊兒具有了一種帶有優先級的層次關係,這對於保證Java程序的穩定運做很重要。例如,類java.lang.Object類存放在JDKjrelib下的rt.jar之中,所以不管是哪一個類加載器要加載此類,最終都會委派給啓動類加載器進行加載,這邊保證了Object類在程序中的各類類加載器中都是同一個類。