1. 程式人生 > >java 的 ClassLoader 類載入機制詳解

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);
//  }
}