天天看點

JVM類加載機制、類加載器、雙親委派JVM類加載

JVM類加載

類加載過程

  • 加載

    這一步驟通常由JVM提供的類加載器來完成,我們也可以通過實作ClassLoder來自定義一個類加載器。

    指将類的class檔案讀入記憶體當中,并為之建立一個java.lang.Class的對象。

  • 連接配接

    1. 驗證
      驗證是連接配接階段的第一步,這一步是為了保證目前Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,不會危害虛拟機自身的安全。
       1. 檔案格式驗證
           - 是否以魔數(0xCAFEBABE)開頭
           - 主次版本号是否在目前虛拟機處理範圍内
           - 常量池的常量中是否有不被支援的常量類型(檢查常量tag标志)
           - 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
           - CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的資料。
           - Class檔案中各個部分及檔案本身是否有被删除的或附加的其他資訊。
           - ......
       2. 中繼資料驗證
           - 驗證這個類是否有父類,除了java.lang.Object外,其他類必須有父類。
           - 這個類是否繼承了不允許被繼承的類(如被final修飾的類)
           - 如果此類不是抽象類,是否實作了父類或接口的所有要求實作的方法。
           - 類中的屬性是否與父類中的屬性産生了沖突(如覆寫了父類的final屬性、不符合規則的方法重載,例如方法名相同但參數清單一緻)
           - ......
       3. 位元組碼驗證
           主要目的是通過資料流和控制流來分析其語義是合法的、符合邏輯的。這個階段将對方法體進行校驗(中繼資料驗證主要是對類進行驗證)。
           保證被校驗的類在運作時不會對虛拟機産生危害。
           - 保證任意時刻操作數棧的資料類型與指令代碼序列都能配合工作。說白了就是操作數棧中放置了int類型的資料,本地變量表卻存放了long類型的資料。
           - 保證跳轉指令不會跳轉到方法體以外的位元組碼指令上。
           - 保證方法體中轉換是有效的。例如把父類對象指派給子類類型、把對象指派給與這個對象毫不相關的類型等。
           - ......
       4. 符号引用驗證
           符号引用就是在一個類中,對其他類的引用,在将代碼編譯成class檔案時,編譯器并不知道引用類的實際記憶體位址,這時隻能用一個唯一的符号來進行表示。
           在解析階段會将符号引用替換成直接引用。  
           這一步驟主要時為了保證符号引用類能被正确通路到,不會出現類無法通路、非法通路等情況。  
           - 符号引用通過字元串的全限定名是否能找到對于的類。
           - 在指定類中是否存在符合方法的字段描述以及簡單名稱所描述的方法和字段。
           - 符号引用中的類、字段、方法的通路性(public、private、protected、default)是否可被目前類通路。
           - ......
                 
    2. 準備

      這一階段主要是将類中的靜态變量配置設定記憶體,并設定預設初始值。

      注意:設定的預設初始值并不是代碼中實際的值,例如int = 1,這時設定的值并不是1,而是0(因為是int,預設0)。

    3. 解析

      這一階段主要就是将類中的符号引用替換成直接引用。

      直接引用:指向目标的指針,偏移量或者能夠直接定位的句柄。該引用是和記憶體中的布局有關的,并且一定要加載進來的。

  • 初始化

    這一階段主要是将靜态變量進行指派,此處賦的值與準備過程中賦的值不同,準備中賦的值為預設初始值,而這裡是代碼中真實的值。

    注意:初始化是有順序的,按照寫的代碼的順序向下初始化。且不能通路還未被變量初始化的靜态變量。

    // 此寫法中,初始化完畢後i最終的值為0。因為先将i指派為1,然後static代碼塊将i又指派為0。
        static int i = 1;
        static {
            i = 0;
        }
        
        // 此寫法中,初始化完畢後i最終的值為1。因為static代碼塊将i指派為0。然後将i又指派為1。
        static {
            i = 0;
        }
        static int i = 1;
        
         // 此寫法中,将在System.out.print(i)這一行報錯。
         // 因為不能通路還未被變量初始化的值。但可以指派(i=0賦的值将被變量指派那一行覆寫掉,如案例2中結果一樣)
        static {
            i = 0;
            System.out.print(i);
        }
        static int i = 1;
               

類加載器

  1. 根類加載器

    根類加載器由C++編寫,負責加載$JAVA_HOME中jre/lib裡所有的class

  2. 擴充加載器

    它負責加載JRE的擴充目錄,lib/ext或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類。由Java語言實作。

  3. 系統類加載器

    它負責将 使用者類路徑(java -classpath或-Djava.class.path變量所指的目錄,即目前類所在路徑及其引用的第三方類庫的路徑)下的類庫 加載到記憶體中。

雙親委派機制

親委派機制,其工作原理的是,如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行。
如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終将到達頂層的啟動類加載器。
如果父類加載器可以完成類加載任務,就成功傳回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載。

雙親委派機制的優勢:采用雙親委派模式的是好處是Java類随着它的類加載器一起具備了一種帶有優先級的層次關系,
通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。
其次是考慮到安全因素,java核心api中定義類型不會被随意替換,假設通過網絡傳遞一個名為java.lang.Integer的類,
通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,并不會重新加載網絡傳遞的過來的java.lang.Integer,而直接傳回已加載過的Integer.class,
這樣便可以防止核心API庫被随意篡改。