Java類的加載機制

   相信很多人對Java類的加載機制,都是雲裏霧裏的,包括我也是,還有許多初學者,前些天剛好遇到了有個小姐姐,談到jvm,她初始認爲,jvm編譯的字節碼文件,是能夠被cpu所能識別讀到內存中去的,正好這塊也雲裏霧裏,所以,就花了一些時間,去把這塊弄明白。

1、字節碼文件

在聊 Java 類加載機制之前,需要先了解一下 Java 字節碼,因爲它和類加載機制息息相關,可能我們都知道,jvm將我們的.java文件編譯成.class文件。

我們知道,計算機只認識 0 和 1,所以任何語言編寫的程序都需要編譯成機器碼才能被計算機理解,然後執行,Java 也不例外。

Java 在誕生的時候喊出了一個很牛X的口號:“Write Once, Run Anywhere”,爲了達成這個目的,Sun 公司發佈了許多可以在不同平臺(Windows、Linux)上運行的 Java 虛擬機(JVM)——負責載入和執行 Java 編譯後的字節碼。

那麼Java字節碼文件到底是什麼樣子的,我們寫一個類,然後執行javac編譯,看一下對應的class文件是什麼內容。

package com.cmower.java_demo;

public class Test {

    public static void main(String[] args) {
        System.out.println("沉默王二");
    }

}

我們編譯這個類,然後打開對應的class文件,看一下里邊的內容。

cafe babe 0000 0034 0022 0700 0201 0019  .......4."......
636f 6d2f 636d 6f77 6572 2f6a 6176 615f  com/cmower/java_
6465 6d6f 2f54 6573 7407 0004 0100 106a  demo/Test......j
6176 612f 6c61 6e67 2f4f 626a 6563 7401  ava/lang/Object.
0006 3c69 6e69 743e 0100 0328 2956 0100  ..<init>...()V..
0443 6f64 650a 0003 0009 0c00 0500 0601  .Code...........
000f 4c69 6e65 4e75 6d62 6572 5461 626c  ..LineNumberTabl

這段字節碼中的 cafe babe 被稱爲“魔數”,是 JVM 識別 .class 文件的標誌。還有一些類似亂碼我們看不懂的東西。

2、類的加載過程

瞭解了 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 進行實例化。

3、類加載器

一般來說,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。所以沒有輸出。

4、雙親委派模型

如果以上三種類加載器不能滿足要求的話,程序員還可以自定義類加載器(繼承 java.lang.ClassLoader 類),它們之間的層級關係如下圖所示。

這種層次關係被稱作爲雙親委派模型:如果一個類加載器收到了加載類的請求,它會先把請求委託給上層加載器去完成,上層加載器又會委託上上層加載器,一直到最頂層的類加載器;如果上層加載器無法完成類的加載工作時,當前類加載器纔會嘗試自己去加載這個類。

PS:雙親委派模型突然讓我聯想到朱元璋同志,這個同志當上了皇帝之後連宰相都不要了,所有的事情都親力親爲,只有自己沒精力沒時間做的事才交給大臣們去幹。

使用雙親委派模型有一個很明顯的好處,那就是 Java 類隨着它的類加載器一起具備了一種帶有優先級的層次關係,這對於保證 Java 程序的穩定運作很重要。

上文中曾提到,如果兩個類的加載器不同,即使兩個類來源於同一個字節碼文件,那這兩個類就必定不相等——雙親委派模型能夠保證同一個類最終會被特定的類加載器加載。

5、最後

這也是通過網上學習瞭解,最後整理總結過來,不管任何事,只要你勇於去研究,去鑽研,當你瞭解之後,就會發現不是那麼迷茫的,雖然還有許多許多的未知、還是得需要多學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章