JVM的類加載機制是指虛擬機html
把描述類的數據從class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型的實現過程。java
類加載過程具體能夠分紅下面幾個步驟:程序員
(1)裝載:查找和導入Class文件;數據庫
(2)連接:把類的二進制數據合併到JRE中;
校驗:檢查載入Class文件數據的正確性;
準備:給類的靜態變量分配存儲空間,賦默認值;
解析:將符號引用轉成直接引用;數組
(3)初始化:對類的靜態變量,靜態代碼塊執行初始化操做。安全
加載是類加載過程的第一個階段,
在加載階段,虛擬機須要完成如下工做:
經過一個類的全限定名來獲取其定義的二進制字節流;
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
在 Java 堆中生成一個表明這個類的 java.lang.Class 對象,做爲對方法區中這些數據的訪問入口。網絡
注意,這裏的二進制字節流並不僅是單純地從 Class 文件中獲取,好比它還能夠從 Jar 包中獲取、從網絡中獲取、由其餘文件生成(JSP 應用)等。
相對於類加載的其餘階段而言,加載階段是可控性最強的階段,由於開發人員既可使用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載。
加載階段完成後,虛擬機外部的 二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在 Java 堆中也建立一個 java.lang.Class 類的對象,這樣即可以經過該對象訪問方法區中的這些數據。數據結構
類加載器用於實現類的加載動做,但它在 Java 程序中起到的做用卻不限於類的加載階段。atom
對於任意一個類,都須要由它的類加載器和這個類自己一同肯定其在就 Java 虛擬機中的惟一性,也就是說,即便兩個類來源於同一個 Class 文件,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。spa
這裏的「相等」包括了表明類的 Class 對象的 equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用 instanceof 關鍵字對對象所屬關係的斷定結果。
Java 開發人員的角度來看,類加載器能夠大體劃分爲如下三類:
啓動類加載器,Bootstrap ClassLoader:
負責加載存放在JDK\jre\li(JDK 表明 JDK 的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,而且能被虛擬機識別的類庫(如 rt.jar,全部的java.*開頭的類均被 Bootstrap ClassLoader 加載)。
啓動類加載器是沒法被 Java 程序直接引用的。
擴展類加載器,Extension ClassLoader:
該加載器由sun.misc.LauncherE
AppClassLoader 來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,
若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
應用程序都是由這三種類加載器互相配合進行加載的,若是有必要,咱們還能夠加入自定義的類加載器。
由於 JVM 自帶的 ClassLoader 只是懂得從本地文件系統加載標準的 java class 文件,所以若是編寫了本身的 ClassLoader,即可以作到以下幾點:
在執行非置信代碼以前,自動驗證數字簽名。
動態地建立符合用戶特定須要的定製化構建類。
從特定的場所取得 java class,例如數據庫中和網絡中。
這種層次關係稱爲類加載器的雙親委派模型。
雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,
所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。
使用雙親委派模型來組織類加載器之間的關係,主要是虛擬機出於穩定和安全性的考慮。
例如,類java.lang.Object 類存放在JDK\jre\lib下的 rt.jar 之中,所以不管是哪一個類加載器要加載此類,最終都會委派給BootstrapClassLoader進行加載,這邊保證了 Object 類在程序中的各類類加載器中都是同一個類。
試想若是一我的寫了一個惡意的基礎類(如java.lang.String)並加載到JVM將會引發嚴重的後果,但有了全盤負責制,java.lang.String永遠是由根裝載器來裝載,避免以上狀況發生。
類裝載器有載入類的需求時,會先請示其Parent使用其搜索路徑幫忙載入,若是Parent 找不到,那麼才由本身依照本身的搜索路徑搜索類,
下面舉一個例子來講明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Public
class
Test{
Public
static
void
main(String[] arg){
ClassLoader c = Test.
class
.getClassLoader();
//獲取Test類的類加載器
System.out.println(c);
ClassLoader c1 = c.getParent();
//獲取c這個類加載器的父類加載器
System.out.println(c1);
ClassLoader c2 = c1.getParent();
//獲取c1這個類加載器的父類加載器
System.out.println(c2);
}
}
|
運行結果:
...AppClassLoader...
...ExtClassLoader...
Null
能夠看出Test是由AppClassLoader加載器加載的。
AppClassLoader的Parent 加載器是 ExtClassLoader 可是ExtClassLoader的Parent爲 null ,須要注意的是,
Bootstrap Loader是用C++語言寫的,邏輯上並不存在Bootstrap Loader的類實體,因此在java程序代碼裏試圖打印出其內容時,咱們就會看到輸出爲null。
這個過程能夠繼續分爲三個階段:
驗證的目的是爲了確保 Class 文件中的字節流包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
不一樣的虛擬機對類驗證的實現可能會有所不一樣,但大體都會完成如下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。
文件格式的驗證:驗證字節流是否符合 Class 文件格式的規範,而且能被當前版本的虛擬機處理,
該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內。通過該階段的驗證後,字節流纔會進入內存的方法區中進行存儲,後面的三個驗證都是基於方法區的存儲結構進行的。
元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合 Java 語法規範的元數據信息。
字節碼驗證:該階段驗證的主要工做是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會作出危害虛擬機安全的行爲。
符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化爲直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身之外的信息(常量池中的各類符號引用)進行匹配性的校驗。
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有如下幾點須要注意:
這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。
這裏所設置的初始值一般狀況下是數據類型默認的零值(如 0、0L、null、false 等),而不是被在 Java 代碼中被顯式地賦予的值。
假設一個類變量的定義爲:
1
|
public
static
int
value =
3
;
|
那麼變量 value 在準備階段事後的初始值爲 0,而不是 3,由於這時候還沒有開始執行任何 Java 方法,而把 value 賦值爲 3 的 putstatic 指令是在程序編譯後,存放於類構造器 ()方法之中的,因此把 value 賦值爲 3 的動做將在初始化階段纔會執行。
下表列出了 Java 中全部基本數據類型以及 reference 類型的默認零值:
這裏還須要注意以下幾點:
對基本數據類型來講,對於類變量(static)和全局變量,若是不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來講,在使用前必須顯式地爲其賦值,不然編譯時不經過。
對於同時被 static 和 final 修飾的常量,必須在聲明的時候就爲其顯式地賦值,不然編譯時不經過;
而只被 final 修飾的常量則既能夠在聲明時顯式地爲其賦值,也能夠在類初始化時顯式地爲其賦值,總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值。
對於引用數據類型 reference 來講,如數組引用、對象引用等,若是沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null。
若是在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。
若是類字段的字段屬性表中存在 ConstantValue 屬性,即同時被 final 和 static 修飾,那麼在準備階段變量 value 就會被初始化爲 ConstValue 屬性所指定的值。
假設上面的類變量 value 被定義爲:
1
|
public
static
final
int
value =
3
;
|
編譯時 Javac 將會爲 value 生成 ConstantValue 屬性,在準備階段虛擬機就會根據 ConstantValue 的設置將 value 賦值爲 3。咱們能夠理解爲 static final 常量在編譯期就將其結果放入了調用它的類的常量池中。
解析階段是虛擬機將常量池中的符號引用轉化爲直接引用的過程,這裏要針對Class文件中的不一樣內容進行區別處理,
解析動做主要針對類或接口、字段、類方法、接口方法四類符號引用進行,
分別對應於常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info 四種常量類型。
一、類或接口的解析:判斷所要轉化成的直接引用是對數組類型,仍是普通的對象類型的引用,從而進行不一樣的解析。
二、字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,若是有,則查找結束;
若是沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,尚未,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束。
三、類方法解析:對類方法的解析與對字段解析的搜索步驟差很少,只是多了判斷該方法所處的是類仍是接口的步驟,並且對類方法的匹配搜索,是先搜索父類,再搜索接口。
四、接口方法解析:與類方法解析步驟相似,知識接口不會有父類,所以,只遞歸向上搜索父接口就好了。
初始化是類加載過程的最後一步,到了此階段,才真正開始執行類中定義的 Java 程序代碼。
在準備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員經過程序指定的主觀計劃去初始化類變量和其餘資源。
在Java中,對於類有且僅有四種狀況會對類進行初始化。
(1)使用new關鍵字實例化對象的時候,讀取或設置一個類的靜態字段時候(除final修飾的static外),調用類的靜態方法時候,都只會初始化該靜態字段或者靜態方法所定義的類。
(2)使用reflect包對類進行反射調用的時候,若是類沒有進行初始化,則先要初始化該類。
(3)當初始化一個類的時候,若是其父類沒有初始化過,則先要觸發其父類初始化。
(4)虛擬機啓動的時候,會初始化一個有main方法的主類。
注意:
經過子類引用父類靜態字段,只會初始化父類不會初始化子類;
經過數組定義來引用類,也不會觸發該類的初始化;
常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,所以也不會觸發定義常量的類的初始化。
聯想String.Intern()操做。
轉:http://www.cnblogs.com/binyue/p/4736558.html