Java JVM——2.類載入器子系統
概述
類載入器子系統在Java JVM中的位置
類載入器子系統的具體實現
類載入器子系統的作用
① 負責從檔案系統或者網路中載入.class檔案,Class 檔案在檔案開頭有特定的檔案標識。
② ClassLoader只負責Class 檔案的載入,至於它是否可以執行,則由Execution Engine決定。
③ Class 檔案載入到JVM中,被稱為DNA元資料模板,存放在方法區。 除了類的資訊外,方法區中還會存放執行時常量池資訊,可能還包括字串字面量和數字常量(這部分常量資訊是Class檔案中常量池部分的記憶體對映)。
④ 從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>方法完畢。其它執行緒被喚醒後不會再進入<clinit>方法,同一個類載入器下,一個型別只會初始化一次。
在發生以下幾種情況時,JVM 不會執行類的初始化流程:
★ 常量在編譯時會將其常量值存入使用該常量的類的常量池中,該過程不需要呼叫常量所在的類,因此不會觸發該常量類的初始化。
★ 在子類引用父類的靜態欄位時,不會觸發子類的初始化,只會觸發父類的初始化。
★ 定義物件陣列,不會觸發該類的初始化。
★ 在使用類名獲取 Class 物件時不會觸發類的初始化。
★ 在使用 Class.forName 載入指定的類時,可以通過 initialize 引數設定是否需要對類進行初始化。
★ 在使用 ClassLoader 預設的 loadClass 方法載入類時不會觸發該類的初始化。
類載入器
JVM 提供了 3 種類載入器,分別是啟動類載入器、擴充套件類載入器和應用程式類載入器,還有一種是使用者自定義類載入器。
如圖所示:
JVM 類載入器
① 啟動類載入器:
- 這個類載入使用C/C++語言實現的,巢狀在JVM內部。
- 它用來載入Java的核心庫(JAVAHOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身需要的類。
- 並不繼承自ava.lang.ClassLoader,沒有父載入器。
- 載入擴充套件類和應用程式類載入器,並指定為他們的父類載入器。
- 出於安全考慮,Bootstrap啟動類載入器只加載包名為java、javax、sun等開頭的類。
② 擴充套件類載入器:
- Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現。
- 派生於ClassLoader類。
- 父類載入器為啟動類載入器。
- 從java.ext.dirs系統屬性所指定的目錄中載入類庫,或從JDK的安裝目錄的jre/1ib/ext子目錄(擴充套件目錄)下載入類庫。如果使用者建立的JAR放在此目錄下,也會自動由擴充套件類載入器載入。
③ 應用程式類載入器(系統類載入器):
- java語言編寫,由sun.misc.LaunchersAppClassLoader實現。
- 派生於ClassLoader類。
- 父類載入器為擴充套件類載入器。
- 它負責載入環境變數classpath或系統屬性java.class.path指定路徑下的類庫。
- 該類載入是程式中預設的類載入器,一般來說,Java應用的類都是由它來完成載入。
- 通過classLoader#getSystemclassLoader() 方法可以獲取到該類載入器。
④ 自定義的類載入器:
在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類的方式都被看作是對類的被動使用,都不會導致類的初始