Java虛擬機類加載機制是把Class類文件加載到內存,並對Class文件中的數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的java類型的過程。
java可動態擴展的語言特性就是依賴運行期動態加載和動態鏈接這個特點實現的。
類從被加載到虛擬機內存中開始,到卸載出內存為止,整個生命周期包括:加載、驗證、准備、解析、初始化、使用和卸載七個階段,其中驗證、准備、解析3個部分統稱為連接,加載、驗證、准備、初始化、卸載這5個階段的順序是一定的,類的加載過程必須按照這種順序按部就班的開始,但是並不一定按部就班的執行,因為這些階段通常都是互相交叉的混合式進行的,通常會在一個階段執行的過程中調用、激活另外一個階段,而且解析階段在某些情況下可以在初始化階段之後進行,這是為了支持java語言的動態綁定。
虛擬機規范嚴格規定了有且只有5種情況必須立即對類進行初始化:(稱為對類的主動引用)
(1)遇到new、getstatic、putstatic和invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發器初始化,
(2)使用java.lang.reflect包的方法對類進行反射調用的時候,
(3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,
(4)當虛擬機啟動時,用戶需要指定一個要執行的主類,虛擬機會先初始化這個主類,
(5)當使用jdk1.7的動態語言進行支持的時候,如果一個java.lang.invoke.methodHandle實例最後的解析結果ref_getstatic等的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先觸發其初始化。
除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用:
(1)通過子類引用父類的靜態字段,不會導致子類初始化,對於靜態字段,只有直接定義這個字段的類才會被初始化,
(2)通過數組定義來引用類,不會觸發此類的初始化
(3)常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化,
接口的加載和類的加載稍有一些不同,但是接口也有初始化的過程,這一點與類是一致的,上面的代碼都是用靜態語句塊“static{}”來輸出初始化信息,而接口不能使用“static{}”語句塊,但編譯器仍然會為接口生成“
類加載的過程:
在加載階段,java虛擬機需要完成以下3件事:
a.通過一個類的全限定名來獲取定義此類的二進制字節流。
b.將定義類的二進制字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構。
c.在java堆中生成一個代表該類的java.lang.Class對象,作為方法區數據的訪問入口。
一個非數組類的加載階段(即加載階段中獲取類的二進制字節流的動作)是開發人員可以控制的,但是對於數組類,情況有所不同,數組類本身不通過類加載器創建,他是由java虛擬機直接進行創建的,但是數組類的元素類型最終是要靠類加載器去創建,一個數組類的創建過程遵循的規則參考p215頁
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,方法區的數據存儲格式由虛擬機自行定義,
驗證階段:
驗證是連接階段的第一步,目的是為了確保class文件的字節流中包含的心細符合當前虛擬機的要求,並且不會危害虛擬機自身的安全,java語言本身是相對安全的語言,使用純粹的java代碼無法做到諸如訪問數組邊界以外的數據,如果這樣做了,編譯器將拒絕編譯,但是class文件並不一定要求用java源碼編譯而來,可以使用任何途徑產生,所以虛擬機如果不檢查輸入的字節流,對其完全信任的話,很可能會因為載入了有害的字節流而導致系統崩潰,所以驗證是虛擬機對自身保護的一項重要工作。
驗證的4個階段:
1.文件格式驗證:
驗證是否以魔數0xcafebabe開頭,主、次版本號是否在當前虛擬機處理范圍之內,常量池的常量是否有不被支持的敞亮類型等,主要目的是保證輸入的字節流能正確的解析並存儲於方法區之內,
2.元數據驗證:
對字節碼描述的信息進行語義分析,保證信息符合java語言規范要求,主要包括的驗證點為:這個類是否有父類,這個類的父類是否繼承了不允許被繼承的類,如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法等
3.字節碼驗證:
主要目的是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的,第二階段對數據信息中的數據類型做完校驗後,這個階段將對類的方法體進行校驗分析,如果一個類的方法體通過了字節碼驗證,也不能說明其一定是安全的,
由於數據流驗證的高復雜性,避免消耗過多的時間,虛擬機在方法體的code屬性的屬性表增加了一項名為“stackmaptable”的屬性,描述了方法體中所有的基本塊開始時本地變量表和操作棧應有的狀態,就不用根據程序推導這些狀態的合法性 p218
4.符號引用驗證:
目的是確保解析動作能正常執行,發生在虛擬機將符號引用轉換為直接引用的時候,
准備階段:
准備階段是正式為類變量分配內存並設置類變量初始值的階段,首先這裡的類變量不包括實例變量,實例變量將會在對象實例化的時候隨著對象一起分配在java堆中,初始值一般是數據類型的零值,但是如果類字段的字段屬性表中存在constantvalue(不變)屬性,那麼就會在准備階段初始化為屬性所指定的值,
解析階段:
解析是虛擬機將常量池內的符號引用替換為直接引用的過程,符號引用與虛擬機實現的內存布局無關,對同一個符號引用進行多次解析是很常見的事情,除invokedynamic外,虛擬機可以對第一次解析的結果進行緩存,從而避免解析動作重復進行,但是對於invokedynamic指令,因為其目的是用於動態語言支持,
四種引用的解析過程:
1.類或接口的解析:
2.字段解析:
3.類方法解析:
4.接口方法解析
初始化階段:
類初始化階段是類加載的最後一步,到了初始化階段,才真正開始執行類中定義的java程序代碼,在准備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,是執行類構造器
首先看一下
(1)
(2)
(3)由於父類的
(4)
(5)執行接口中的
Java虛擬機的類加載是通過類加載器實現的,對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機中的唯一性,也就是說比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義,
(1).BootStrap ClassLoader:啟動類加載器,負責加載存放在%JAVA_HOME%\lib目錄中的,或者通被-Xbootclasspath參數所指定的路徑中的,並且被java虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫,即使放在指定路徑中也不會被加載)類庫到虛擬機的內存中,啟動類加載器無法被java程序直接引用。
(2).Extension ClassLoader:擴展類加載器,由sun.misc.Launcher$ExtClassLoader實現,負責加載%JAVA_HOME%\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
(3).Application ClassLoader:應用程序類加載器,由sun.misc.Launcher$AppClassLoader實現,負責加載用戶類路徑classpath上所指定的類庫,是類加載器ClassLoader中的getSystemClassLoader()方法的返回值,開發者可以直接使用應用程序類加載器,如果程序中沒有自定義過類加載器,該加載器就是程序中默認的類加載器。
注意:上述三個JDK提供的類加載器雖然是父子類加載器關系,但是沒有使用繼承,而是使用了組合關系。
從JDK1.2開始,java虛擬機規范推薦開發者使用雙親委派模式(ParentsDelegation Model)進行類加載,其加載過程如下:
(1).如果一個類加載器收到了類加載請求,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器去完成。
(2).每一層的類加載器都把類加載請求委派給父類加載器,直到所有的類加載請求都應該傳遞給頂層的啟動類加載器。
(3).如果頂層的啟動類加載器無法完成加載請求,子類加載器嘗試去加載,如果連最初發起類加載請求的類加載器也無法完成加載請求時,將會拋出ClassNotFoundException,而不再調用其子類加載器去進行類加載。
雙親委派 模式的類加載機制的優點是java類它的類加載器一起具備了一種帶優先級的層次關系,越是基礎的類,越是被上層的類加載器進行加載,保證了java程序的穩定運行。雙親委派模式的實現:若要實現自定義類加載器,只需要繼承java.lang.ClassLoader類,並且重寫其findClass()方法即可。java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然後從這些字節代碼中定義出一個Java類,即java.lang.Class類的一個實例。除此之外,ClassLoader還負責加載Java應用所需的資源,如圖像文件和配置文件等,ClassLoader中與加載類相關的方法如下:
方法
說明