概述
虛擬機把描述類的數據從Class文件加載到內存中,并對數據進行驗證,準備,解析,初始化的一個過程,最終是可以被虛擬機直接使用的java類型,這就是類加載的一個簡單的過程。
Java中的類加載是在運行時加載,這樣會比較的消耗性能,但是正是在運行時加載使得java擁有很好的靈活性和可擴展性。
類加載的時機
類從被加載到內存中開始,到卸載出內存為止。它的生命周期總共七個階段:加載---->驗證---->準備---->解析---->初始化---->使用---->卸載。其中解析這個過程是不確定的,它可能會在初始化后之后,這是為了使java支持運行時的綁定。
- new ,getstatic,putstatic,invokestatic這四條指令時會觸發初始化的操作。
- new是new一個新的對象時會觸發初始化。
- getstatic是獲取靜態字段時會觸發。
- putstatic是設置靜態字段時會觸發。
- invokestatic是調用另一個類的靜態方法的時候。
PS:需要注意的是getstatic和putstatic被final修飾的,在編譯期就放入到常量池中是不會觸發的。 - 使用java.lang.reflect的包方法對類進行反射調用時,如果類沒有初始化就需要進行初始的操作。
- 子類進行初始化時需要對父類先進行初始。
- java啟動時需要的啟動主類,程序的入口。該類就需要進行初始化。
- 使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.Methondhandle實例最后解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,如果沒有進行初始化時會觸發初始化。
PS:接口的初始化和類初始化不同,接口初始化只和類初始化的子類初始化是需要父類先進行初始化,而且并不是接口父類中的所有都是會初始化。
加載
加載是類加載中前面提到的其中的一個過程。類加載的基本過程:
- 通過全限定類名加載二進制流。
- 將二進制流代表的靜態存儲結構轉換方法區中運行時的數據結構。
- 在內存中生成java.lang.Class對象,將這個作為該方法區這個類中各種數據的一個入口。
加載分為數組類加載過程和非數組類的加載過程。java的數組類的加載過程其實是有虛擬機直接加載的但是數組中的類型需要類加載機制加載:
- 非數組類加載機制:可控性強既可以有系統類加載器進行加載又可以由用戶自定義的類加載器進行加載。(重寫一個類加載器的loadClass()方法)。
- 數組類型的加載機制:數組類型的加載機制如果是引用類型,就使用遞歸進行加載,并且會在加載的類型上加入一個標志。如果是非引用類型則會把標志與引導類加載器關聯。
ps:數組類的可見性與它組件的可見性是相同的,如果組件類型不是引用類型的可見性一般設置為public。
類加載完成會有一個連接,可能在沒完成加載就開始連接,雖然如此但是該順序是一定的。
驗證
驗證的主要目的是保證加載進來的Class文件的字節流包含的信息符合虛擬機的當前的要求,不會有危害自身的數據存在。
Java是相對C++語言是安全的語言,例如它有C++不具有的數組越界的檢查。這本身就是對自身安全的一一種保護。驗證階段是Java非常重要的一個階段,它會直接的保證應用是否會被惡意入侵的一道重要的防線,越是嚴謹的驗證機制越安全。驗證的四個階段文件格式驗證-->元數據驗證-->字節碼驗證-->符號引用驗證。
- 文件格式驗證:主要驗證字節流是否符合Class文件格式規范,并且能被當前的虛擬機加載處理。
- 是否以魔數開頭。
- 主,次版本號是否在當前虛擬機處理的范圍之內。
- 常量池中是否有不被支持的常量類型。
- 指向常量的中的索引值是否存在不存在的常量或不符合類型的常量。
- CONSTANT_Utf8_info型的常量中有不符合utf8格式的編碼數據。
還有大其它的驗證這里就不一一的列舉。 - 元數據驗證:對字節碼描述的信息進行語義的分析,分析是否符合java的語言語法的規范。
- 字節碼驗證:最重要的驗證環節,分析數據流和控制,確定語義是合法的,符合邏輯的。主要的針對元數據驗證后對方法體的驗證。保證類方法在運行時不會有危害出現。
- 符號引用驗證:主要是針對符號引用轉換為直接引用的時候,是會延伸到第三解析階段,主要去確定訪問類型等涉及到引用的情況,主要是要保證引用一定會被訪問到,不會出現類等無法訪問的問題。
雖然驗證很重要但是并不是必須的階段。當然大量重復的驗證會相當的花費性能和時間的。
準備
準備階段主要是類變量進行分配內存和數據的初始化階段,所謂的初始化并不是你編碼時所定義的變量值。例如:
public static int age = 20;
數據的初始化并不會將它初始化為20,而是初始化為0,系統有一套自己的初始化值。如下圖:
數據類型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | 0 |
char | '\u0000' |
byte | 0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
當然會有特殊的情況,如下面的代碼:
public static final int value = 20;
這種情況是類的字段時存在ConstantValue屬性所指定的字段。用final修飾后出現該屬性,加初始化時會直接的使用ConstantValue的屬性值,所以會初始化為20。
解析
解析是將常量池中的符號引用轉化為直接引用的過程,還記得前面驗證階段時出現的符號引用驗證嗎?就是對該階段的驗證。
- 符號引用:符號引用是以一組符號來描述所引用的目標,符號可以是任何的字面形式的字面量,只要不會出現沖突能夠定位到就行。布局和內存無關。
- 直接引用:是指向目標的指針,偏移量或者能夠直接定位的句柄。該引用是和內存中的而布局有關的,并且一定加載進來的。
虛擬機可能會多次的進行解析。解析主要的對類,接口,字段,類方法,接口方法,方法類型,方法句柄和調用點限定符引用進行。這七種解析有細節上的不同,主要的思想是通過限定性類名找到解析的類型進行解析。主要的是會分為數組類型,非數組類型存在一個直接進行解析的過程。在過程還有從下上的匹配查找(主要出現在有繼承,接口的情況下)。
初始化
初始化算是類加載過程的最后一個階段,在這個階段在是真正的開始有java代碼主導。大家應該記得在準備階段已經進行過一次賦值,但是只是系統的默認賦值(ConstantValue的例外情況)。初始化是執行<clinit>的過程。
- <clinit>的主要是查找static模塊,用戶自定義類變量的賦值,該順序是由文件中的順序界定的。加載過程存在的是父類的一定會比子類先進行加載到,因為會保證子類的<clinit>加載完成時父類的<clinit>一定會加載完成。所有就像大家所知道的java.lang.object一定會是虛擬機中第一個加載完成的。
- <clinit>在接口中的加載是不同的它是不存在靜態塊的,接口中也是會有賦值進行的,但是接口中的是在需要用到才會去進行加載的。
- 允許在定義之前進行賦值的操作,但是不允許使用,如下:
public class A{
static{
s = 20;
//system.out.printf(s);
上面注釋的這句話時會出現錯誤的;
}
static int s = 10;
}
- 虛擬機會保證在多線程的環境下進行加鎖,保證正確執行。如果有多個進行加載一個會保證只有一個去加載,其他的會進去阻塞等待中。同一個類只會加載一次,就算多個進入阻塞也不會重新喚醒。
類加載器
- 類與類加載器:一個類的相同判斷條件大家都知道,但是如果不是由同一個類加載器加載出來的,就算是看起來相同的也是出現false的。
- 三大類加載器:
- 啟動類加載器
- 擴展類加載器
- 應用程序類加載器
-
雙親委托機制:
雙器委托機制
雙親委托機制是當一個類進入加載時,子加載器不會自己嘗試去加載,而是將其發送到它的父加載器中加載,以此類推直到達到最后的加載器,只有當父加載器不能進行加載是會發送到子加載器中,子加載此時才會嘗試去加載。
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;
}
- 雙親委托機制的破壞
- 1.2版本為了向前兼容1.0版本
- 本身模型的問題,基礎類要調用用戶類而出現的沖突。通過設置線程上下文類加載器,如果出現上面這種情況,通過上下文類加載器去加載所需的類。
- 用戶對動態性的追求,出現沒一個模塊都有自己的類加載器,如果需要更換時連同類加載器一同換掉。