本文從類七個階段更加high Level的角度去解析一下類的加載過程。Java字節碼的表現形式是字節數組,而java類在jvm中的表現是java.lang.Class對象。大家都知道java字節碼能夠在jvm中被使用,需要經過加載,鏈接,初始化三個步驟,而其中開發人員只能接觸到java類的加載,利用類的加載器可以在運行時動態的加載一個類。這也是java的一個非常重要的特點。
Java類的加載
java類的加載是由類加載器完成的 。類加載器分為兩種:啟動類加載器(引導、擴展和系統)和自定義類加載器。其中啟動類加載器是jvm原生的,自定義類加載器是通過繼承java.lang.ClassLoader類實現的,java.lang.ClassLoader提供了一些基本的實現,用戶只需要實現必要的幾個方法就可以了。
java類加載器有兩個非常重要的特征,也是保證同一個類可以被不同的類引用的重要保障:層次結構和代理模式,層次結構是指每個類加載器都有一個父類加載器(引導類加載器除外),代理模式是指每個類加載器對類的加載可以代理給其他類加載器,這也就導致一個類啟動過程中的類加載器和最終定義這個類的類加載器可能不是同一個。真正完成類的加載工作是通過調用defineClass來實現的,而啟動類的加載過程是通過調用loadClass來實現的。在 Java 虛擬機判斷兩個類是否相同的時候,使用的是類的定義加載器。也就是說,哪個類加載器啟動類的加載過程并不重要,重要的是最終定義這個類的加載器。方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。類加載器在成功加載某個類之后,會把得到的 java.lang.Class類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對于一個類加載器實例來說,相同全名的類只加載一次,即 loadClass方法不會被重復調用,但是不能保證多線程的加載只有一次。
Java類的鏈接
Java類的鏈接包含三個階段:驗證,準備,解析,是將Java類的二進制代碼合并到JVM運行狀態的過程。在鏈接之前,這個類必須被成功加載。驗證就是確保二進制結構符合JVM的要求。準備是指創建Java類中的靜態域,并設置為默認值。解析是確保這些被引用的類可以被正確的找到,解析的過程可能導致其他Java類被加載。
不同的JVM實現可能選擇不同的解析策略。一種做法是在鏈接的時候,就遞歸的把所有依賴的形式引用都進行解析。而另外的做法則可能是只在一個形式引用真正需要的時候才進行解析。也就是說如果一個Java類只是被引用了,但是并沒有被真正用到,那么這個類有可能就不會被解析。考慮下面的代碼:
public class LinkTest {
public static void main(String[] args) {
ToBeLinked toBeLinked = null;
System.out.println("Test link.");
}
}
toBeLinked被引用了,但是并沒有真正用到它,編譯好之后刪除toBeLinked.class,程序不會出錯。
Java類的初始化
當一個Java類第一次被真正用到的時候,JVM會進行該類的初始化操作。初始化過程的主要操作是執行靜態代碼塊和初始化靜態域。在一個類被初始化之前,它的直接父類也需要被初始化。但是,一個接口的初始化,不會引起其父接口的初始化。在初始化的時候,會按照源代碼中從上到下的順序依次執行靜態代碼塊和初始化靜態域。
創建自己的類加載器
在 Java應用開發過程中,可能會需要創建應用自己的類加載器。典型的場景包括實現特定的Java字節代碼查找方式、對字節代碼進行加密/解密以及實現同名 Java類的隔離等。創建自己的類加載器并不是一件復雜的事情,只需要繼承自java.lang.ClassLoader類并覆寫對應的方法即可。 java.lang.ClassLoader中提供的方法有不少,下面介紹幾個創建類加載器時需要考慮的:
- defineClass():這個方法用來完成從Java字節代碼的字節數組到java.lang.Class的轉換。這個方法是不能被覆寫的,一般是用原生代碼來實現的。
- findLoadedClass():這個方法用來根據名稱查找已經加載過的Java類。一個類加載器不會重復加載同一名稱的類。
- findClass():這個方法用來根據名稱查找并加載Java類。
- loadClass():這個方法用來根據名稱加載Java類。
- resolveClass():這個方法用來鏈接一個Java類。
這里比較 容易混淆的是findClass()方法和loadClass()方法的作用。前面提到過,在Java類的鏈接過程中,會需要對Java類進行解析,而解析可能會導致當前Java類所引用的其它Java類被加載。在這個時候,JVM就是通過調用當前類的定義類加載器的loadClass()方法來加載其它類的。findClass()方法則是應用創建的類加載器的擴展點。應用自己的類加載器應該覆寫findClass()方法來添加自定義的類加載邏輯。 loadClass()方法的默認實現會負責調用findClass()方法。
前面提到,類加載器的代理模式默認使用的是父類優先的策略。這個策略的實現是封裝在loadClass()方法中的。如果希望修改此策略,就需要覆寫loadClass()方法。