<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                ![](https://img.kancloud.cn/cc/93/cc9362c6a21586e7e014b7df2641bb2e_407x319.png) [TOC] ## 1 概述 類加載機制:JVM將描述類的數據從 class 文件中加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型 ## 2 類加載的時機 - 類加載的生命周期分為七個階段:加載、驗證、準備、解析、初始化、使用、卸載 - 驗證、準備、解析三個階段都處于連接(Linking)階段 - 加載、驗證、準備、初始化和卸載這五個階段的順序是確定的 有且只有六種情況必須立即對類進行“初始化” 1. 遇到 `new`、`getstatic`、`putstatic`、`invokestatic` 這四個字節碼指令時,如果類型沒有進行過初始化,則需要觸發其初始化 - 使用 `new` 關鍵字實例化對象的時候 - 讀取或設置一個類型的靜態字段(被 `final` 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候 - 調用一個類型的靜態方法的時候 2. 使用 `java.lang.reflect` 包的方法對類型進行反射調用的時候,如果類型沒有進行過初始化,則需要觸發其初始化 3. 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化 4. 當虛擬機啟動時,會初始化包含 `main()` 方法的主類 5. `JDK 7` 加入動態語言支持后,如果一個 `java.lang.invoke.MethodHandles` 實例最后的解析結果為 `REF_getStatic`、`REF_putStatic`、`REF_invokeStatic`、`REF_newInvokeSpecial` 四種類型的方法句柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化 6. `JDK 8` 加入默認方法后,當一個接口定義了被 `default` 修飾的接口方法時,如果這個接口的實現類發生了初始化,則該接口需要在其之前被初始化 > **有且僅有**以上6種情況會進行類的初始化,以下是一個關于不進行類初始化的例子 ```java public class ClinitTest { public static void main(String[] args) { // 1. 對常量的引用,不會觸發類的初始化,無輸出 int v1 = SubClass.SUPER_CONSTANTS; // 2. 通過數組引用類,不會觸發類的初始化,無輸出 SuperClass[] arr = new SuperClass[10]; // 3. 通過子類引用父類字段,只會對父類進行初始化,輸出 // System.out.println(SubClass.superField); // 4. SubClass.value 未被引用時,不會調用 SubClass#getValue 方法,無輸出 // 5. 引用 SubClass 的常量不會觸發 SubClass,但是 value 需要通過 SubClass 的 static 方法獲取, // 所以將觸發 SubClass 的初始化,子類初始化會先觸發父類的初始化,所以會先初始化 SuperClass (打印Super,再打印Sub) // 接著調用 getValue 方法,打印 getValue int v2 = SubClass.value; } } /** * 父類 */ class SuperClass { static { System.out.println("Super"); } /** 常量 */ public static final int SUPER_CONSTANTS = 1; /** static 變量 */ public static int superField = 2; } /** * 子類 */ class SubClass extends SuperClass { static { System.out.println("Sub"); } public static final int value = getValue(); private static int getValue() { System.out.println("getValue"); return 5; } } ``` ## 3 類加載的過程 ### 3.1 加載(Loading) 在加載階段,JVM需要完成: 1. 通過一個類的全限定名來獲取定義此類的二進制字節流 2. 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構 3. 在內存中生成一個代表這個類的 `java.lang.Class` 對象,作為方法區這個類的各種數據的訪問入口 獲取二進制字節流,可通過多種方式: - 通過zip包,如jar、ear、war格式等 - 從網絡獲取,如Web Applet - 運行時計算生成,如動態代理,`java.lang.reflect.Proxy` 類中的 `byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);` - 從其他文件生成,例如將JSP文件生成class文件 - 從數據庫讀取,例如SAP Netweaver - 從加密文件中獲取,例如防class文件被反編譯的保護措施,通過加載時解密class文件來保障程序的運行邏輯不被窺探 數組類型不通過類加載器創建,通過JVM直接在內存中動態構造出來,一個數組類(簡稱C)創建過程遵循以下規則: - 一個數組的組件類型(Component Type)指的是數組去掉一個維度的類型 - 如果數組的組件類型是引用類型,則遞歸地去加載這個組件類型,數組C將被標識在類加載器的類名稱空間上 - 如果數組的組件類型不是引用類型(例如int[]的組件類型是int),則Java虛擬機將把數組C標記為與引導類加載器關聯 - 數組類的可訪問性與組件類型的可訪問性一致,如果組件類型不是引用類型,則其數組類的可訪問性默認為public,例如可在任何(Java 9引入模塊化之前)類或接口使用int[] ### 3.2 驗證(Verification) 如果虛擬機驗證到輸入的字節流不符合class文件格式的約束,就應當拋出一個 `java.lang.VerifyError` 異常或者子類異常,驗證階段大致會完成以下四個階段的驗證: 1. 文件格式驗證 2. 元數據驗證 3. 字節碼驗證 4. 符號引用驗證 **文件格式驗證**主要驗證:字節流是否符合class文件格式的規范,并且能被當前版本的虛擬機處理,可能包含: - 是否以 `0xcafebabe` 開頭 - 主、次版本號是否在當前JVM接受范圍內 - 常量池的常量是否有不被支持的類型(tag標記) - 指向常量的各種索引值是否有指向不存在的常量或不符合類型的常量 - `CONSTANT_Utf8_info` 類型的常量是否有不符合UTF-8編碼的數據 - class文件中各個部分即文件本身是否有被刪除、或者附加的其他信息 - …… **元數據驗證**是對字節碼描述的信息進行語義分析,以保證其描述的信息符合《Java語言規范》的要求,可能包含: - 該類是否有父類(除了java.lang.Object外,所有的類都應當有父類) - 該類是否繼承了不允許被繼承的類(被final修飾的類) - 如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法 - 類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載) - …… **字節碼驗證**最復雜的一個驗證階段,主要是通過數據流分析和控制流分析,確定程序語義是合法的、符合邏輯的,主要對類的方法體(class文件中的Code屬性)進行校驗分析,例如: - 保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,例如不會出現在操作數棧放置一個int數據類型的數據,使用時卻按long類型來加載入本地變量表中的情況 - 保證任何跳轉指令都不會跳轉到方法體以外的字節碼指令上 - 保證方法體中的類型轉換總是有效的 - …… **符號引用驗證**發生在虛擬機將符號引用轉化為直接引用的時候,可以看作對類自身外的各類信息進行匹配性校驗,通常包含: - 符號引用中通過字符串描述的全限定名是否能找到對應的類 - 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段 - 符號引用中的類、字段、方法的可訪問性是否可被當前類所訪問 - …… ### 3.3 準備(Preparation) 準備階段是正式為類中定義的變量(static修飾的變量)分配內存并設置類變量初始值的階段,這些變量所使用的內存都應當在方法區中進行分配。需要注意以下幾種情況: - 此處的類變量,不包括實例變量,實例變量會隨著對象實例化分配到堆中 - 假設一個類變量定義為 `static int value = 123`,則準備階段過后的初始值為0而不是123,因為此時尚未開始執行任何Java方法(把value賦值為123的動作是在類的初始化階段,其 `putstatic` 指令存放于類構造器`<clinit>()`方法中) - 假設一個類變量定義為 `static fianl int value = 123`,則準備階段該變量就會被初始化為ConstantValue屬性所指定的初始值,即123 以下是基本數據類型的零值: 數據類型 | 零值 | | 數據類型 | 零值 ---- | ---- | ---- | ---- | ---- int | 0 | | boolean | false long | 0L | | float | 0.0f short | (short) 0 | | double | 0.0d char | '\u0000' | | reference | null byte | (byte) 0 | | | ### 3.4 解析(Resolution) 解析階段是JVM將常量池內的符號引用替換為直接引用的過程,以下是符號引用和直接引用的關聯如下: - 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可 - 直接引用(Direct References):直接引用是可以直接指向目標的指針、相對偏移量或者是一個能間接定位到目標的句柄。 《Java虛擬機規范》中并未對規定解析階段發生的具體時間,只要求了在執行以下17個**用于操作符號引用的字節碼指令**之前,先對它們所使用的符號引用進行解析:`anewarray`、`checkcast`、`getfield`、`getstatic`、`instanceof`、`invokednamic`、`invokeinterface`、`invokespecial`、`invokestatic`、`invokevirtul`、`ldc`、`ldc_w`、`ldc2_w`、`multianewarray`、`new`、`putfield`、`putstatic` 解析動作主要針對類或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、類方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_InterfaceMethodref_mic_info)、方法類型(CONSTANT_MethodType_info)、方法句柄(CONSTANT_MethodHandle_info)和調用點限定符(CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic_info)這7類符號引用進行。 #### 3.4.1 類或接口的解析 將一個類D中從未解析過的符號引用N解析為一個類或接口C的直接引用,則解析過程包含3個步驟: 1. 解析非數組類型:使用D的類加載器,根據N的全限定名加載類C,并觸發相關類的加載操作,出現異常則解析過程失敗 2. 解析數組類型:遞歸地按照第一點的規則加載數組元素類型,由虛擬機生成一個代表該數組維度和元素的數組對象 3. 進行符號引用驗證,確認D是否對C具備訪問權限,如果發現不具備,將拋出 `java.lang.IllegalAccessError` 異常 `JDK 9` 引入模塊化以后,一個public類型不再意味著程序任何位置都有它的訪問權限,還必須檢查模塊間的訪問權限,如果D擁有C的訪問權限,則意味著以下規則至少有1條成立: - C為public且與D處于同一模塊 - C為public且不與D處于同一模塊,但是C的模塊允許D的模塊訪問 - C不為public但是與D處于同一package中 #### 3.4.2 字段解析 要解析一個未被解析過的字段引用,首先會對字段表內 `class_index` 項中索引的 `CONSTANT_Class_info` 符號引用進行解析,也就是字段所屬的類或接口的符號引用。如果解析過程出現了任何異常,則解析失敗;如果解析成功,則要按照《Java虛擬機規范》的要求,對該字段的類或接口(用C表示)進行后續字段的搜索: 1. 如果C本身就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束;否則進入第2步 2. 如果在C中實現了接口,將會按照繼承關系從下往上遞歸搜索各個接口和父接口,如果父接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束;否則進入第3步 3. 如果C不是 `java.lang.Obejct` ,將會按照繼承關系從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束,否則進入第4步 4. 查找失敗,拋出 `java.lang.NoSuchFieldError` 異常 如果查找過程成功返回了引用,將會對這個字段進行權限驗證,如果發現不具備對該字段的訪問權限,將拋出 `java.lang.IllegalAccessError` 異常 #### 3.4.3 類方法解析 要解析一個未被解析過的方法引用,首先會對方法表內 `class_index` 項中索引的 `CONSTANT_Class_info` 符號引用進行解析,也就是方法所屬的類或接口的符號引用。如果解析過程出現了任何異常,則解析失敗;如果解析成功,則要按照《Java虛擬機規范》的要求,對該方法的類或接口(用C表示)進行后續方法的搜索: 1. 由于類方法(CONSTANT_Methodref_info)和接口方法(CONSTANT_InterfaceMethodref_mic_info)的常量類型定義是分開的,因此如果在類的方法表中發現class_index中索引的C是個接口的話,則拋出 `java.lang.IncompatiibleClassChangeError` 異常 2. 如果第1步通過,則在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束;否則進入第3步 3. 在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束;否則進入第4步 4. 在類C的接口列表以及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束,否則進入第5步 5. 查找失敗,拋出 `java.lang.NoSuchMethodError` 異常 如果查找過程中成功返回了引用,將會對這個方法進行權限驗證,如果發現不具備對該方法的訪問權限,將拋出 `java.lang.IllegalAccessError` 異常 #### 3.4.4 接口方法解析 要解析一個未被解析過的接口方法引用,首先會對接口方法表內 `class_index` 項中索引的 `CONSTANT_Class_info` 符號引用進行解析,也就是方法所屬的類或接口的符號引用。如果解析過程出現了任何異常,則解析失敗;如果解析成功,則要按照《Java虛擬機規范》的要求,對該接口方法的類或接口(用C表示)進行后續接口方法的搜索: 1. 由于類方法(CONSTANT_Methodref_info)和接口方法(CONSTANT_InterfaceMethodref_mic_info)的常量類型定義是分開的,因此如果在接口的接口方法表中發現class_index中索引的C是個類的話,則拋出 `java.lang.IncompatiibleClassChangeError` 異常 2. 如果第1步通過,則在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個接口方法的直接引用,查找結束;否則進入第3步 3. 在接口C的父接口中遞歸查找,直到 `java.lang.Object` 類為止,看是否有簡單名稱和描述符都與目標相匹配的接口方法,如果有則返回這個接口方法的直接引用,查找結束;否則進入第4步 4. 由于接口允許多繼承,因此如果C的不同父類接口中存有多個簡單名稱和描述符都與目標相匹配的方法,將從這多個方法中返回其中一個并查找結束(《Java虛擬機規范》中沒有進一步規則約束,但是不同廠商實現的Javac可能會對此作限制),否則進入第5步 5. 查找失敗,拋出 `java.lang.NoSuchMethodError` 異常 如果查找過程中成功返回了引用,將會對這個方法進行權限驗證,如果發現不具備對該方法的訪問權限,將拋出 `java.lang.IllegalAccessError` 異常(在JDK 9之前,Java 接口中所有的方法默認public,也沒有模塊化的約束,不存在訪問權限問題;但是JDK 9之后增加了接口的靜態私有方法,也有了模塊化的約束,則對接口方法的訪問也可能拋出該異常) ### 3.5 初始化 在類的初始化階段,JVM開始真正執行類中編寫的Java程序代碼。 - 準備階段會為變量賦予零值,初始化階段會根據程序代碼初始化類變量和其他資源。 - 初始化階段就是執行類構造器`clinit<>()`方法的過程 `clinit<>()` 方法是Javac的產物,以下是關于`clinit<>()`方法的原理介紹: - `clinit<>()` 方法是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊中的語句,合并產生的;收集順序與源文件中出現的順序一致,靜態語句塊只能訪問在其之前出現的變量,**在其之后出現的變量,靜態語句塊可以賦值,但是不能訪問** - `clinit<>()` 方法與類的構造函數`init<>()`不同,它不需要顯示地調用父類構造器,JVM會保證在子類的 `<clinit>()` 方法執行之前,其父類的 `<clinit>()` 方法已經執行完畢 - 因此JVM中第一個被執行的 `clinit<>()` 方法一定是 `java.lang.Object` 類的 - 因此父類中定義的靜態語句塊要優先于子類的變量賦值操作 - `clinit<>()` 方法對類或接口來說不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成 `<clinit>()` 方法 - 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成 `clinit<>()` 方法,但是: - 執行接口的 `clinit<>()` 方法不需要先執行其父接口的 `clinit<>()` 方法,只有其父接口中定義的變量被使用時,父接口才會被初始化 - 接口的實現類在初始化的過程中,也不會執行接口的 `clinit<>()` 方法 - JVM必須保證一個類的 `clinit<>()` 方法是線程安全的 > 示例:父類中定義的靜態語句塊要優先于子類的變量賦值操作 ```java class Parent { static int parentVar = 1; static { parentVar = 2; } } class Child extends Parent { static int childVar = parentVar; } public static void main(String[] args) { System.out.println(Child.childVar); // 成功編譯,輸出2 } ``` > 示例:JVM必須保證一個類的 `clinit<>()` 方法是線程安全的,因此可以借助這種機制實現一種單例模式 ```java /** * 單例類 */ public class Singleton { /** * 將Singleton的實例化放在Holder的初始化過程中,JVM保證線程安全 */ static class Holder { private static Singleton instance = new Singleton(); } /** * 獲取Singleton的唯一實例 * * @return Singleton的唯一實例 */ public static Singleton getInstance() { return Holder.instance; } /** * 隱藏Singleton的實例化操作 */ private Singleton() { } } ``` ## 4 類加載器 ### 4.1 類與類加載器 - 對于任意一個Class,都必須由加載它的ClassLoader和這個Class本身來確定其在JVM中的唯一性 - 每個ClassLoader,都擁有一個獨立ClassNameSpace - 比較兩個Class是否相等包括: - equeals()方法 - isAssignableFrom()方法 - isInstance()方法 - instanceof關鍵字 ### 4.2 雙親委派模型 > JDK 1.2以來,Java一直保持著三層類加載器、雙親委派的類加載架構(以下內容不包括JDK 9之后版本) **三層類加載器** 類加載器名稱 | 加載路徑 | 說明 ---- | ---- | ---- Boostrap Class Loader | `JAVA_HOME/lib` 或者被 `-Xbootclasspath` 參數指定的路徑 | C++實現,虛擬機自身的一部分 Extension Class Loader | `JAVA_HOME/lib/ext` 或者被 `java.ext.dirs` 系統變量指定的路徑 | `sun.misc.Launcher$ExtClassLoader` 實現 Application Class Loader | `CLASSPATH` 路徑 | `sub.misc.Launcher$AppClassLoader` 實現 **雙親委派模型(Parents Delegation Model)** - 過程:如果一個類收到了類加載的請求,首先將請求委派給父類加載器完成,每一層都是如此,當父類加載器反饋無法完成這個請求時(搜搜范圍內沒有該類,拋出異常),子加載器才會去嘗試自行完成加載 - 特點:Java中的類隨著類加載器一起具備了一種帶有優先級層次的關系,例如java.lang.Object,存放在rt.jar中,無論哪個類加載器去加載它,都會轉到Bootstrap Class Loader去加載,保證了其唯一性 - 注意:即使用自定義類加載器,用defineClass()方法強行加載一個以`java.lang`開頭的類,也不會成功,將收到虛擬機內部拋出的`java.lang.SecurityException` 異常 > `java.lang.ClassLoader#loadClass` 中雙親委派模型的實現 ```java Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class c = findLoadedClass(name); if (c == null) { c = (parent != null) ? parent.loadClass(name, false) : findBootstrapClassOrNull(name); if (c == null) { c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } ``` ### 4.3 破壞雙親委派模型 > 雙親委派模型**并不是一個具有強制性約束**的模型,而是Java設計者推薦給開發者的類加載器實現方式。在JDK 9之前,雙親委派模型出現過三次較大規模的被破壞的情況。 **三次被破壞的情況** - 類加載器的概念和 `java.lang.ClassLoader` 在Java的第一個版本中就已存在,在JDK 1.2之后引入雙親委派模型后,為了兼容已有代碼,使得 `loadClass()` 不被子類覆蓋,設計者們不得不做出妥協,在 ClassLoader 類中添加了一個 protected 方法`findClass()`,并引導用戶去重寫該方法,而不是在 `loadClass()` 中編寫代碼 - 針對基礎類型要調用開發者的代碼,Java團隊設計了Thread Context ClassLoader,該類加載器可以通過 `java.lang.Thread#setContextClassLoader` 進行設置,打通了雙親委派模型的層次結構,以逆向使用類加載器。Java中涉及到SPI(Service Provider Interface)的加載基本上都采用了這種方式來完成,典型例子包括:JNDI、JDBC、JCE、JAXB、JBI。當SPI提供者多于1個時,代碼只能根據具體提供者的類型來硬編碼判斷,為了消除這種影響,在JDK 6提供了 `java.util.ServiceLoader`,以 `META-INF/services` 中的配置信息,輔以責任鏈模式,為SPI的加載提供了一種相對合理的解決方案 - 源于開發者對程序動態性的追求,例如代碼熱替換(Hot Swap)、模塊熱部署(Hot Deployment)。以OSGi為例,OSGi實現模塊化熱部署的關鍵是它自定義的類加載器機制的實現,每一個Bundle(程序模塊)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換 **OSGi類加載過程** 1. 將以java.*開頭的類,委派給父類加載器加載 2. 否則,將委派列表名單內的類,委派給父類加載器加載 3. 否則,將Import列表中的類,委派給Export這個類的Bundle的類加載器加載 4. 否則,查找當前Bundle的ClassPath,使用自己的類加載器加載 5. 否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載 6. 否則,查找Dynamic Import列表的Bundle,委派給對應的Bundle的類加載器加載 7. 否則,類查找失敗
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看