概述
類加載器子系統在Java JVM中的位置
類加載器子系統的具體實現
類加載器子系統的作用
① 負責從文件系統或者網絡中加載.class文件,Class 文件在文件開頭有特定的文件標識。
② ClassLoader只負責Class 文件的加載,至於它是否可以運行,則由Execution Engine決定。
④ 從Class文件-->JVM-->最終成為元數據模板,此過程就要一個運輸工具(類裝載器Class Loader),扮演一個快遞員的角色。
JVM 的類加載機制
類加載過程
JVM 的類加載分為 5 個階段:加載、驗證、准備、解析、初始化。在類初始化完成后就可以使用該類的信息了,當這個類不再被需要時可以從 JVM 中卸載。
類加載過程
加載階段
JVM 讀取 Class 文件,通過一個類的全限定名獲取定義此類的二進制字節流,將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
根據 Class 文件的描述在堆中創建一個代表這個類的 java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。
在讀取 Class 文件時既可以通過文件的形式讀取,也可以通過 jar 包、war 包讀取,還可以通過代理自動生成 Class或其他方式讀取。
驗證階段
主要用於確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,保證被加載類的正確性,進一步保障虛擬機自身的安全,只有通過驗證的 Class 文件才能被 JVM 加載。
主要包括四種驗證:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
准備階段
主要工作是在方法區中為類變量分配內存空間並設置類中變量的初始值。初始值指不同數據類型的默認值,這里需要注意 final 類型的變量和非 final 類型的變量在准備階段的數據初始化過程不同。比如一個成員變量的定義如下:
public static int value = 1500;
在上述代碼中,靜態變量 value 在准備階段的初始值是 0,將 value 設置為 1500 的動作是在對象初始化時完成的,因為 JVM 在編譯階段會將靜態變量的初始化操作定義在構造器中。但是,如果將變量 value 聲明為 final 類型:
public static final int value = 1500;
則 JVM 在編譯階段后會為 final 類型的變量 value 生成其對應的 ConstantValue 屬性,虛擬機在准備階段會根據 ConstantValue 屬性將 value 賦值為 1500。
解析階段
JVM 將常量池內的符號引用轉換為直接引用的過程。事實上,解析操作往往會伴隨着JVM在執行完初始化之后再執行。
符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機規范》的class文件格式中。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等。
初始化階段
主要通過執行類構造器的<clinit>方法為類進行初始化。<clinit>方法是在編譯階段由編譯器自動收集類中靜態語句塊和變量的賦值操作組成的。在准備階段,類中靜態成員變量已經完成了默認初始化,而在初始化階段,<clinit>方法將對靜態成員變量進行顯示初始化。
注意:
1. JVM 規定,只有在父類的<clinit>方法都執行成功后,子類中的<clinit>方法才可以被執行。因此,JVM中第一個被執行<clinit>方法的類肯定是java.lang.Object。
2. 在一個類中既沒有靜態變量賦值操作也沒有靜態語句塊時,編譯器不會為該類生成<clinit>方法。
3. 靜態代碼塊只能訪問到出現在靜態代碼塊之前的變量,定義在它之后的變量,在前面的靜態語句塊可以賦值,但是不能訪問。
4. 接口也需要通過<clinit>方法為接口中定義的靜態成員變量顯示初始化。
5. 接口中不能使用靜態代碼塊,但仍然有變量初始化的賦值操作,因為接口與類一樣都會生成<clinit>方法。不同的是,執行接口的<clinit>方法不需要先執行父接口的<clinit>方法,只有當父接口中的靜態成員變量被使用到時才會執行父接口的<clinit>方法。
6. 虛擬機會保證在多線程環境中一個類的<clinit>方法被正確地加鎖同步。當多條線程同時去初始化一個類時,只會有一個線程去執行該類的<clinit>方法,其它線程都被阻塞等待,直到活動線程執行<clinit>方法完畢。
在發生以下幾種情況時,JVM 不會執行類的初始化流程:
★ 常量在編譯時會將其常量值存入使用該常量的類的常量池中,該過程不需要調用常量所在的類,因此不會觸發該常量類的初始化。
★ 在子類引用父類的靜態字段時,不會觸發子類的初始化,只會觸發父類的初始化。
★ 定義對象數組,不會觸發該類的初始化。
★ 在使用類名獲取 Class 對象時不會觸發類的初始化。
★ 在使用 Class.forName 加載指定的類時,可以通過 initialize 參數設置是否需要對類進行初始化。
★ 在使用 ClassLoader 默認的 loadClass 方法加載類時不會觸發該類的初始化。
類加載器
JVM 提供了 3 種類加載器,分別是啟動類加載器、擴展類加載器和應用程序類加載器,還有一種是用戶自定義類加載器。
如圖所示:
JVM 類加載器
① 啟動類加載器:
- 它用來加載Java的核心庫(JAVAHOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身需要的類。
- 並不繼承自ava.lang.ClassLoader,沒有父加載器。
- 加載擴展類和應用程序類加載器,並指定為他們的父類加載器。
- 出於安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類。
② 擴展類加載器:
- 派生於ClassLoader類。
- 父類加載器為啟動類加載器。
③ 應用程序類加載器(系統類加載器):
- 派生於ClassLoader類。
- 父類加載器為擴展類加載器。
- 它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫。
- 該類加載是程序中默認的類加載器,一般來說,Java應用的類都是由它來完成加載。
④ 自定義的類加載器:
在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,但在必要時,我們還需要自定義類加載器,來定制類的加載方式。我們可以通過繼承 java.lang.ClassLoader 實現自定義的類加載器。
為什么要自定義類加載器?
✔ 隔離加載類
✔ 修改類加載的方式
✔ 擴展加載源
✔ 防止源碼泄漏
兩種類加載器獲取的差異:
JVM 支持兩種類型的類加載器。分別為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)。
規定所有派生於抽象類 ClassLoader的類加載器都被划分為自定義類加載器,ClassLoader是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器),所以啟動類加載器為一類,其余的類加載器為另一類。
我們看一段代碼,獲取它們的類加載器:
public class ClassLoaderTest { public static void main(String[] args) { // 1.獲取:系統類加載器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); // 2.獲取其上層的:擴展類加載器
ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); // 3.試圖獲取:根加載器(啟動類加載器)
ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); // 4.獲取:自定義加載器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); // 5.獲取:String類型的加載器
ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1); } }
運行結果:
我們可以看到,目前用戶代碼所使用的加載器為系統類加載器,其上是擴展類加載器,二者是同屬一類加載,都可以用代碼直接獲取。但是,根加載器(啟動類加載器)無法直接通過代碼獲取。同時,我們通過獲取String類型的加載器,發現是null,那么說明String類型是通過根加載器進行加載的,也就是說Java的核心類庫都是使用根加載器進行加載的。
雙親委派機制
工作原理
Java虛擬機對class文件采用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內存生成class對象。而且加載某個類的class文件時,Java虛擬機采用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式。
雙親委派機制指一個類在收到類加載請求后不會嘗試自己加載這個類,而是把該類加載請求向上委派給其父類去完成,其父類在接收到該類加載請求后又會將其委派給自己的父類,以此類推,這樣所有的類加載請求都被向上委派到啟動類加載器中。若父類加載器在接收到類加載請求后發現自己也無法加載該類(通常原因是該類的 Class 文件在父類的類加載路徑中不存在),則父類會將該信息反饋給子類並向下委派子類加載器加載該類,直到該類被成功加載,若找不到該類,則 JVM 會拋出 ClassNotFoud 異常。
雙親委派過程
沙箱安全機制
自定義String類,但在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載jdk自帶的文件(rt.jar包中java\lang\String.class),報錯信息說沒有main方法,就是因為加載的是rt.jar包中的string類,並不是自定義的String類。這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制。
雙親委派機制的優點
★ 避免類的重復加載
★ 保護程序安全,防止核心API被隨意篡改
☆ 自定義類:java.lang.String(報錯:阻止創建 java.lang開頭的類)
☆ 自定義類:java.lang.ShkStart(報錯:阻止創建 java.lang開頭的類)
如何判斷兩個Class對象是否相等
在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:
☛ 類的完整類名必須一致,包括包名。
☛ 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同。
換句話說,在JVM中,即使兩個類對象(Class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象就是不相等的。
JVM必須知道一個類是由啟動加載器加載的還是由用戶類加載器加載的。如果一個類是由用戶類加載器加載的,那么JVM會將這個類加載器的一個引用作為類型信息的一部分保存在方法區中,當解析一個類到另一個類的引用時,JVM需要保證這兩個類的類加載器是相同的。
類的主動使用和被動使用
Java程序對類的使用方式分為:主動使用和被動使用。
主動使用,有七種情況:
-
創建類的實例
-
訪問某個類或接口的靜態變量,或者對該靜態變量賦值
-
調用類的靜態方法
-
反射(比如:Class.forName("com.atguigu.Test"))
-
初始化一個類的子類
-
Java虛擬機啟動時被標明為啟動類的類
-
JDK7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化
除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化。