很長一段時間裏,我對 Java 的類加載機制都很是的抗拒,由於我以爲太難理解了。但爲了成爲一名優秀的 Java 工程師,我決定硬着頭皮研究一下。java
在聊 Java 類加載機制以前,須要先了解一下 Java 字節碼,由於它和類加載機制息息相關。程序員
計算機只認識 0 和 1,因此任何語言編寫的程序都須要編譯成機器碼才能被計算機理解,而後執行,Java 也不例外。安全
Java 在誕生的時候喊出了一個很是牛逼的口號:「Write Once, Run Anywhere」,爲了達成這個目的,Sun 公司發佈了許多能夠在不一樣平臺(Windows、Linux)上運行的 Java 虛擬機(JVM)——負責載入和執行 Java 編譯後的字節碼。bash
到底 Java 字節碼是什麼樣子,咱們藉助一段簡單的代碼來看一看。網絡
源碼以下:學習
package com.cmower.java_demo;
public class Test {
public static void main(String[] args) {
System.out.println("沉默王二");
}
}
複製代碼
代碼編譯經過後,經過 xxd Test.class
命令查看一下這個字節碼文件。spa
xxd Test.class
00000000: cafe babe 0000 0034 0022 0700 0201 0019 .......4."...... 00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f com/cmower/java_ 00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a demo/Test......j 00000030: 6176 612f 6c61 6e67 2f4f 626a 6563 7401 ava/lang/Object. 00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100 ..<init>...()V.. 00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601 .Code........... 00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c ..LineNumberTabl 複製代碼
感受有點懵逼,對不對?3d
懵就對了。調試
這段字節碼中的 cafe babe
被稱爲「魔數」,是 JVM 識別 .class 文件的標誌。文件格式的定製者能夠自由選擇魔數值(只要沒用過),好比說 .png 文件的魔數是 8950 4e47
。code
至於其餘內容嘛,能夠選擇忘記了。
瞭解了 Java 字節碼後,咱們來聊聊 Java 的類加載過程。
Java 的類加載過程能夠分爲 5 個階段:載入、驗證、準備、解析和初始化。這 5 個階段通常是順序發生的,但在動態綁定的狀況下,解析階段發生在初始化階段以後。
1)Loading(載入)
JVM 在該階段的主要目的是將字節碼從不一樣的數據源(多是 class 文件、也多是 jar 包,甚至網絡)轉化爲二進制字節流加載到內存中,並生成一個表明該類的 java.lang.Class
對象。
2)Verification(驗證)
JVM 會在該階段對二進制字節流進行校驗,只有符合 JVM 字節碼規範的才能被 JVM 正確執行。該階段是保證 JVM 安全的重要屏障,下面是一些主要的檢查。
cafe bene
開頭)。3)Preparation(準備)
JVM 會在該階段對類變量(也稱爲靜態變量,static
關鍵字修飾的)分配內存並初始化(對應數據類型的默認初始值,如 0、0L、null、false 等)。
也就是說,假若有這樣一段代碼:
public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二";
複製代碼
chenmo 不會被分配內存,而 wanger 會;但 wanger 的初始值不是「王二」而是 null
。
須要注意的是,static final
修飾的變量被稱做爲常量,和類變量不一樣。常量一旦賦值就不會改變了,因此 cmower 在準備階段的值爲「沉默王二」而不是 null
。
4)Resolution(解析)
該階段將常量池中的符號引用轉化爲直接引用。
what?符號引用,直接引用?
符號引用以一組符號(任何形式的字面量,只要在使用時可以無歧義的定位到目標便可)來描述所引用的目標。
在編譯時,Java 類並不知道所引用的類的實際地址,所以只能使用符號引用來代替。好比 com.Wanger
類引用了 com.Chenmo
類,編譯時 Wanger 類並不知道 Chenmo 類的實際內存地址,所以只能使用符號 com.Chenmo
。
直接引用經過對符號引用進行解析,找到引用的實際內存地址。
5)Initialization(初始化)
該階段是類加載過程的最後一步。在準備階段,類變量已經被賦過默認初始值,而在初始化階段,類變量將被賦值爲代碼指望賦的值。換句話說,初始化階段是執行類構造器方法的過程。
oh,no,上面這段話說得很抽象,很差理解,對不對,我來舉個例子。
String cmower = new String("沉默王二");
複製代碼
上面這段代碼使用了 new
關鍵字來實例化一個字符串對象,那麼這時候,就會調用 String 類的構造方法對 cmower 進行實例化。
聊完類加載過程,就不得不聊聊類加載器。
通常來講,Java 程序員並不須要直接同類加載器進行交互。JVM 默認的行爲就已經足夠知足大多數狀況的需求了。不過,若是遇到了須要和類加載器進行交互的狀況,而對類加載器的機制又不是很瞭解的話,就不得不花大量的時間去調試 ClassNotFoundException
和 NoClassDefFoundError
等異常。
對於任意一個類,都須要由它的類加載器和這個類自己一同肯定其在 JVM 中的惟一性。也就是說,若是兩個類的加載器不一樣,即便兩個類來源於同一個字節碼文件,那這兩個類就一定不相等(好比兩個類的 Class 對象不 equals
)。
站在程序員的角度來看,Java 類加載器能夠分爲三種。
1)啓動類加載器(Bootstrap Class-Loader),加載 jre/lib
包下面的 jar 文件,好比說常見的 rt.jar。
2)擴展類加載器(Extension or Ext Class-Loader),加載 jre/lib/ext
包下面的 jar 文件。
3)應用類加載器(Application or App Clas-Loader),根據程序的類路徑(classpath)來加載 Java 類。
來來來,經過一段簡單的代碼瞭解下。
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
複製代碼
每一個 Java 類都維護着一個指向定義它的類加載器的引用,經過 類名.class.getClassLoader()
能夠獲取到此引用;而後經過 loader.getParent()
能夠獲取類加載器的上層類加載器。
這段代碼的輸出結果以下:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
複製代碼
第一行輸出爲 Test 的類加載器,即應用類加載器,它是 sun.misc.Launcher$AppClassLoader
類的實例;第二行輸出爲擴展類加載器,是 sun.misc.Launcher$ExtClassLoader
類的實例。那啓動類加載器呢?
按理說,擴展類加載器的上層類加載器是啓動類加載器,但在我這個版本的 JDK 中, 擴展類加載器的 getParent()
返回 null
。因此沒有輸出。
若是以上三種類加載器不能知足要求的話,程序員還能夠自定義類加載器(繼承 java.lang.ClassLoader
類),它們之間的層級關係以下圖所示。
這種層次關係被稱做爲雙親委派模型:若是一個類加載器收到了加載類的請求,它會先把請求委託給上層加載器去完成,上層加載器又會委託上上層加載器,一直到最頂層的類加載器;若是上層加載器沒法完成類的加載工做時,當前類加載器纔會嘗試本身去加載這個類。
PS:雙親委派模型忽然讓我聯想到朱元璋同志,這個同志當上了皇帝以後連宰相都不要了,全部的事情都親力親爲,只有本身沒精力沒時間作的事才交給大臣們去幹。
使用雙親委派模型有一個很明顯的好處,那就是 Java 類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係,這對於保證 Java 程序的穩定運做很重要。
上文中曾提到,若是兩個類的加載器不一樣,即便兩個類來源於同一個字節碼文件,那這兩個類就一定不相等——雙親委派模型可以保證同一個類最終會被特定的類加載器加載。
硬着頭皮翻看了大量的資料,而且動手去研究之後,我發現本身居然對 Java 類加載機制(JVM 將類的信息動態添加到內存並使用的一種機制)不那麼抗拒了——真是蠻奇妙的一件事啊。
也許學習就應該是這樣,只要你勇於挑戰本身,就能收穫知識——就像山就在那裏,只要你肯攀登,就能到達山頂。