java 的 ClassLoader 類加載機制詳解

一個程序要運行,需要經過一個編譯執行的過程:
Java的編譯程序就是將Java源程序 .java 文件 編譯爲JVM可執行代碼的字節碼文件 .calss 。Java編譯器不將對變量和方法的引用編譯爲數值引用,也不確定程序執行過程中的內存佈局,而是將這些符號引用信息保留在字節碼中,由解釋器在運行過程中創立內存佈局,然後再通過查表來確定一個方法所在的地址。這樣就有效的保證了Java的可移植性和安全性。

編譯完之後就需要加載進內存,然後執行
類的加載就是將類的.class文件中的二進制數據讀進內存之中,將其放進JVM運行時內存的方法區中, 然後在堆中創建一個java.lang.Class對象,用於封裝在方法區中類的數據結構,然後 根據這個Class對象,我們可以創建這個類的對象,對象可以有很多個,但是對應的Class對象只有這一個。
備註:關於方法區概念,不懂的,可以看看JVM 的內存機制

這個Class對象就像是一面鏡子一樣,可以反射一個類的內存結構,因此Class對象是整個反射的入口, 可以通過 (對象.getClass(), 類.Class, 或者Class.forName( 類名的全路徑:比如java.lang.String) 三種方式獲取到, 通過class對象,我們可以反射的獲取某個類的數據結構,訪問對應數據結構中的數據,這也是反射機制的基本實現原理。

加載 .class 文件有幾種途徑
1. 可以 從本地直接加載(加載本地硬盤中的.class文件)
2. 通過網絡下載 .class 文件,(java.net.URLClassLoader 加載網絡上的某個 .class文件)
3. 從zip,jar 等文件中加載 .class 文件
4. 將java源文件動態的編譯爲 .class 文件(動態代理)

java的ClassLoader 可以分爲兩大類,
一: Java 自帶的 類加載器
二: 用戶自定義的類加載器 (用戶可以自己寫一個類加載器,但是必須繼承自java.lang.ClassLoader類)
類加載的最終產品 創建一個位於堆中方法區的Class對象,每一個class對象都包含有一個對應的classLoader,可以通過class.getClassLoader() 獲取到 ,
但是注意上面講的例子另外較爲特殊的是:
數組類的類加載器也可以由這個方法返回,但是 該加載器與裏面的元素的類加載器的信息相同,如果裏面的元素爲基本類型,String , void型(jdk 1.5之後將void 納入基本數據類型),那麼返回的類加載器也是null

Java 自帶的類加載器可以分爲三類:
一: 根加載器(Bootstrap): 主要用來加載java的核心API,根加載器加載出來的Class對象,我們是無法訪問到的,底層
* 是C++實現的,JVM也沒有暴露根類加載器
二: 擴展類加載器(Extension): 主要加載java 中擴展的一些jar包中的文件,比如你的項目中放在System Library 裏的jar包
* 或者你導入的其他jar包
三:應用類加載器(AppClassloader): 也叫系統類加載器(system)主要用來加載一些用戶自己寫的類的.class文件

類加載器加載的過程可以分爲三步
第一: 加載 這個階段就是將class文件從文件或者本地存儲中讀取到內存中的過程

第二: 連接主要實現的就是將已經讀入到內存中的二進制class數據合併到JVM的運行時環境中去。在沒有實現這一步之前,class 都是一個個單獨的存放在內存中的二進制文件,他們之間沒有任何聯繫,只有JVM把他們連接起來,才能將每個類之間的關係有機的結合起來。
* 連接又分爲三小步
1. 驗證: 驗證加載類的正確性。 有人說class文件不是JVM編譯成的字節碼嗎,還需要驗證嗎? 答案是肯定的,在上一步讀取class文件的時候,是不做內容檢查的,有可能你把一個文件的後綴名修改爲.class, 它也會被讀進內存,但是在驗證就是要檢驗你的.class文件是不是定義的一個正確的類,是不是通過JVM編譯而成的,當然因爲java是開源的,一些第三方(如CGlib)也可以生成符合加載的字節碼文件, 爲了安全起見,重新在進行一次編譯檢查
驗證實現功能: (類文件的結構檢查,字節碼驗證,二進制兼容性驗證,語義檢查)
2. 準備: 爲類的靜態變量分配內存空間,並初始化這些變量爲一個默認的值。 一定要看清這裏是爲類的靜態變量分配的內存空間而且這裏也有一個初始化的工作(初始化靜態變量爲默認值,int 型的爲 0, boolean 型的爲 false, 對象爲null),與下面的初始化是有區別的。
3. 解析: 將類中的符號引用解析爲直接引用。 在java語言裏,我們說是沒有指針的, 實現通過引用去訪問對象,一般兩種實現方式,(句柄和直接引用)。 在下面的例子中,Woker 類中調用了Car類中的方法, 因此在運行的時候,JVM把這裏的一個符號引用替換爲一個指針(這裏是指替換成真正由C++實現的指針),我們稱之爲直接引用

 class Worker{
    public void dirveCar(){
        Car.run();
    }
 }  

第三: 初始化: 哎,你可能會產生疑問,上面不是有一次初始化了,這次的初始化是幹什麼的? 是爲類的靜態變量賦予正確的初始值 ,注意到,我這裏加了一個正確的初始值,在上面的連接那部分裏,也有一次初始化工作的,當時設置的是默認值,由此引發的不同,在下面的代碼示例中講解了由於兩種初始化工作所造成的一個很驚訝的結果,詳細看下面。
靜態變量的初始化一般有兩種方式:
第一種 在靜態變量的聲明處進行初始化賦值

 private static int a = 2;

第二種 在靜態代碼塊中賦值

 private static int a;
 static{
  a = 2;
 }

類的初始化時機
請千萬注意,一般來講當類加載進JVM中的時候執行到上面的一步 連接 就結束了,這次的初始化工作是要經過一定條件的觸發纔會執行的, 觸發的條件是什麼呢? 是當程序主動使用該類的時候纔會進行爲靜態變量賦予正確初始值的初始化工作

     * **程序主動使用類(六種情況)**:
     * 1. 創建類的實例
     * 2. 訪問某個類或者接口的靜態變量,或者對該靜態變量賦值
     * 3. 調用類的靜態方法(一定是調用當前類的靜態方法或者靜態變量, 比如如果調用子類的一個方法,但是靜態方法或者變量實際上是在父類中的,因此只會初始化父類,調用的這個子類反而不會進行初始化)
     * 4. 反射   Class.forName()
     * 5. 初始化一個類的子類(繼承關係是會先初始化父類,但是如果實現的是接口,並不會先初始化它的接口,只有當程序首次使用接口特定的靜態變量時纔會初始化接口)
     * 6. java虛擬機啓動時被標明爲啓動類的類(含有main 方法,並且是啓動方法的類 )

類的加載時機
類的加載並不像類的初始化工作一樣,必須要等到程序主動調用的時候。
JVM 允許類加載器預料到某個類將要被使用的時候就提前加載它,如果在加載的過程中 遇到了class文件缺失或者錯誤,那麼 在程序主動調用的時候要報告這個linkageError,如果程序一直沒有使用到這個類,那麼類加載器就不會報告錯誤,這個錯誤一般是版本的不兼容型錯誤,比如你在jdk 1.6 編譯下的class文件 加載到 jdk 1.5的環境中就有可能出錯。

類加載器的父親委託機制

這種機制能夠更好的保證java平臺的安全,在此委託機制中,除了虛擬機自帶的根加載器(bootstrap Classloader)之外,其他所有的類加載器有且只有一個父加載器當 java程序請求類加載器 loader1 加載某一個類時,首先委託其父類的加載器進行加載,如果能夠加載,則由父類加載器進行加載,如果不能加載,則由自己進行加載

類加載器的調用順序, 根加載器—-擴展類加載器—–系統(應用)類加載器 —- 自定義的類加載器

父類委託機制 和 類的調用順序 這一切的原因都是出於安全性的考慮, 這樣用戶自定義的不可靠的類加載器無法加載本該由父類加載器加載的可靠的類, 採用這些就避免了不可靠甚至惡意的代碼加載那些java的核心類庫,從而保證了類加載的安全性。
舉例子: java.lang.Object 由根類加載器加載,如果沒有父類委託機制,那麼你自定義的類加載器就可以加載這個類,那麼程序就會變得極其的不安全,不穩定。

根加載器 : 無父類加載器,並沒有繼承java.lang.ClassLoader類,加載java的核心庫 ,比如java.lang
擴展加載器: 其父類加載器是根加載器,加載jre/lib/ext中的類或者系統屬性中指定的目錄 java.ext.dirs
應用類加載器(系統類加載器) : 其父類加載器是擴展類加載器; 從環境變量path路徑中或者 java.class.path 指定目錄中加載類, 同時他也是用戶自定義的類加載器的默認父類加載器

父類加載器機制並不意味着各個子類加載器繼承了父類的加載器,他們其實是一種組合關係,一種包裝關係。

Classloder Loader1 = new myClassloader();
// 將loader1 作爲 loader2 的父親加載器 (由此可以看出兩者並不是繼承關係), 如果你自定義了一個加載器,但是沒有寫下面的這句,給它指定父類加載器,那麼默認的父類加載器是系統類加載器
ClassLoader loader2 = new myClassloader(loader1)
// 其實是在ClossLoader類中都擁有着一個 protected 變量parent,代表着它的父親加載器, 將上面一句實際執行的是  parent = loader1;

運行時包: 同一個運行包是指類在同一個包下,並且由同一種類加載器實際加載。 安全考慮,原因在下面
這裏寫圖片描述

兩個由不同類加載器加載的類(這兩個類之間無父親委託關係)相互之間是不可見的,只能通過反射訪問 但是子加載器加載的類能夠看見父加載器加載的類,我們平常編程的時候,基本上都是SystemcloassLoader 加載的,在同一個命名空間內,而且其他的類都是由 系統加載器的父類加載的,我們也可以訪問 java.lang.String 或者其他的類。

這裏寫圖片描述

此外類加載還採用了****cache機制,也就是如果 cache中保存了這個Class就直接返回它,如果沒有才從文件中讀取和轉換成Class,並存入cache,這就是爲什麼我們修改了Class但是必須重新啓動JVM才能生效的原因。

類加載器的卸載: java虛擬機自帶的類加載器所加載的類,在虛擬機的生命週期內,是不可能被卸載的,因爲有JVM始終指向他們,但是用戶自定義的類加載器是可以被卸載的。卸載的方式是 無引用指向它們

loader1 = null;
clazz = null; // 一個class對象始終引用它的類加載器
object = null; // 一個實例對象始終引用它的class對象

這裏寫圖片描述

package ClassLoder;

public class ClassLoderTest {
    public static void main(String[] args) {
        /*
         * 8種基本數據類型 及其 對象型, String類型, void 類型的 他們的類加載器是boot(根)加載器
         * boot類加載器,是無法被我們所獲得的,因此java 給我們返還給一個null
         */
        ClassLoader stringLoader = String.class.getClassLoader();
        ClassLoader intLoader = int.class.getClassLoader();
        ClassLoader IntegerLoader = Integer.class.getClassLoader();
        System.out.println(stringLoader); // null
        System.out.println(intLoader); // null 
        System.out.println(IntegerLoader); // null 


        // 是不是覺得結果很詭異,這就是因爲是兩個不同的初始化工作所造成的不同結果,
        // 當調用了 LoaderTest 的靜態方法, 就會對該類的靜態變量進行初始化的工作,首先將 a =0, b = 5,
        //           然後在爲loader1賦值 執行了 +1 操作, a = 1, b = 6
        // 當調用了LoaderTest2 的靜態方法,順序的爲靜態變量賦值,那麼首先賦值的就是 loader2 對象,
        //          但是注意的是此時的 a 和 b 還沒有賦予真正的初始值,他們的值仍舊爲默認的 0, 執行了加1 ,a和b都成了 1
        //          然後執行下面的語句,由對a 與 b 的值進行了覆蓋,a = 0, b = 5
        LoaderTest.getLoderTest();
        LoaderTest.getA();  // 輸出結果 1, 6
        LoaderTest2.getLoderTest2(); 
        LoaderTest2.getA();  // 輸出結果 0, 5

        // 加上靜態代碼塊之後 結果分別爲 2, 7 
        //                          1,  6
       // 這是因爲類在加載的時候順序是 靜態變量初始化的工作和靜態代碼塊的工作就是按照在代碼中出現的順序依次執行的

    }
}
// 測試加載的過程順序
class LoaderTest{
    static int a;
    static int b = 5;
    private static LoaderTest  loader1 = new LoaderTest();
    public LoaderTest(){
        a++;
        b++;
    }
    public static void getA(){
        System.out.println(a +"     " + b);
    }
    public static LoaderTest getLoderTest(){
        return  loader1;
    }
    // 靜態代碼快是在值被初始化之後纔開始執行的,  但是優先於構造函數和其他方法執行
//  static{
//      a++;
//      b++;
//      System.out.println("static 執行了");
//      System.out.println(a +"     " + b); 
//  }
}

class LoaderTest2{
    private static LoaderTest2  loader2 = new LoaderTest2() ;
    private  LoaderTest2(){
        a++;
        b++;
    }
    private static int a = 0;
    private static int b = 5;
    public static void getA(){
        System.out.println(a +"     " + b);
    }
    public static LoaderTest2 getLoderTest2(){
        return  loader2;
    }
//  static{
//      a++;
//      b++;
//      System.out.println("static2 執行了");
//      System.out.println(a +"     " + b);
//  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章