JVM-類加載子系統
類加載子系統
注意:方法區只有HotSpot虛擬機有
類加載器子系統負責從文件系統或者網絡中加載Class文件,class文件在文件開頭有特定的文件標識。當中的類加載器只負責class文件的加載,至於它是否可以運行,則由Execution Engine(執行引擎)決定。加載的類信息存放於一塊稱為方法區的內存空間。除了類的信息外,方法區中還會存放運行時常量池信息,可能還包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)
舉例來說類加載子系統就像是一個中央快遞站,當快遞被打包好(編譯後)發送過來的時候,去進行接收,首先收到快遞看是什麼類型的快遞(順豐,郵政等),不同的快遞由不同的人員去接收(不同的類由不同的加載器去加載),接收完成後要進行驗證,看是不是有什麼損壞(鏈接階段—-驗證),當一切無誤後為該快遞貼上取貨碼(鏈接階段—-準備:初始化一些信息比如類變量),再然後查看快遞所要去往的地方,由快遞員去派送(鏈接階段—-解析:將常量池內的符號引用轉換為直接引用的過程),到達目的快遞站後交由本地快遞站進行處理(進入到初始化階段),快遞可以由快遞站直接送往顧客家裡(類的被動使用),也可以由顧客主動來領(類的主動使用例如:創建類的實例,調用類的靜態方法等).
當然在類的加載階段還有雙親委派機制,在後面會提到.
- class file(編譯後的文件)存在於本地硬盤上,可以理解為設計師畫在紙上的模板,而最終這個模板在執行的時候是要加載到JVM當中來根據這個文件實例化出n個一模一樣的實例。
- class file加載到JVM中,被稱為DNA元數據模板放在方法區。
- 在.class文件–>JVM–>最終成為元數據模板,此過程就要一個運輸工具(類裝載器Class Loader),扮演一個快遞員的角色。
public class Loader {
public static void main(String[] args) {
System.out.println("謝謝ClassLoader加載我....");
}
}
對於上面的代碼他的加載過程是什麼樣呢?
- 首先要想執行 main() 方法(靜態方法)就需要先加載main方法所在類 Loader
- 如果加載成功,則進行鏈接、初始化等操作。完成後調用 Loader類中的靜態方法 main
- 加載失敗則會拋出異常
鏈接分為三個子階段:驗證 -> 準備 -> 解析
- 目的在於確保Class文件的位元組流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全
- 主要包括四種驗證,文件格式驗證,元數據驗證,位元組碼驗證,符號引用驗證。
比如說如果你查看java編譯後的位元組碼文件就會發現,它們的開頭都是CAFE BABE(很多人稱之為咖啡寶貝),如果出現不合法的位元組碼文件,那麼將會驗證不通過。
- 為類變量(static變量)分配內存並且設置該類變量的默認初始值
- 當然這裡不包含用final修飾的static,因為final在編譯的時候就會分配好了默認值,準備階段會顯式初始化
- 同時要注意這裡不會為實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一起分配到Java堆中
舉例來說 查看編譯後的文件
編譯前
package com.Demo; public class ClassInitTest { public static int num = 3; public static void main(String[] args) { System.out.println(ClassInitTest.num); } }
編譯後
public com.Demo.ClassInitTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 //可以看到在初始化階段默認賦了初值為0 1: invokespecial #1 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/Demo/ClassInitTest;
static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=1, locals=0, args_size=0 0: iconst_3 //在初始化階段才賦值為了3 1: putstatic #3 // Field num:I 4: return LineNumberTable: line 4: 0 }
- 將常量池內的符號引用轉換為直接引用的過程
- 事實上,解析操作往往會伴隨着JVM在執行完初始化之後再執行
- 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機規範》的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄
- 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
通過反編譯class文件可以查看到符號引用
#1 = Methodref #6.#23 // java/lang/Object."<init>":()V #2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream; #3 = Fieldref #5.#26 // com/Demo/ClassInitTest.num:I #4 = Methodref #27.#28 // java/io/PrintStream.println:(I)V #5 = Class #29 // com/Demo/ClassInitTest #6 = Class #30 // java/lang/Object #7 = Utf8 num #8 = Utf8 I #9 = Utf8 <init> #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 this #15 = Utf8 Lcom/Demo/ClassInitTest; #16 = Utf8 main #17 = Utf8 ([Ljava/lang/String;)V #18 = Utf8 args #19 = Utf8 [Ljava/lang/String;
類的初始化時機有:
- 創建類的實例
- 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
- 調用類的靜態方法
- 反射(比如:Class.forName(「TestClass」))
- 初始化一個類的子類
- Java虛擬機啟動時被標明為啟動類的類
- JDK7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化
除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化,即不會執行初始化階段(不會調用 clinit() 方法和 init() 方法)
clinit()方法
- 初始化階段就是執行類構造器方法
<clinit>()
的過程 - 此方法不需定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊中的語句合併而來。也就是說,當我們代碼中包含static變量的時候,就會有clinit方法
<clinit>()
方法中的指令按語句在源文件中出現的順序執行<clinit>()
不同於類的構造器。(關聯:構造器是虛擬機視角下的<init>()
)- 若該類具有父類,JVM會保證子類的
<clinit>()
執行前,父類的<clinit>()
已經執行完畢 - 虛擬機必須保證一個類的
<clinit>()
方法在多線程下被同步加鎖
JVM嚴格來講支持兩種類型的類加載器 。分別為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)
從概念上來講,自定義類加載器一般指的是程序中由開發人員自定義的一類類加載器,但是Java虛擬機規範卻沒有這麼定義,而是將所有派生於抽象類ClassLoader的類加載器都劃分為自定義類加載器
可以看到所有ClassLoader下的所有派生類都是屬於自定義類加載器,包括擴展類加載器(Extension ClassLoader)以及系統類加載器(Application ClassLoader)
在程序中我們最常見的類加載器只有3個分別是ExtClassLoader,AppClassLoader,用戶自定義加載器
啟動類加載器(引導類加載器,Bootstrap ClassLoader)
- 這個類加載使用C/C++語言實現的,嵌套在JVM內部
- 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身需要的類
- 並不繼承自java.lang.ClassLoader,沒有父加載器
- 加載擴展類和應用程序類加載器,並作為他們的父類加載器
- 出於安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類
擴展類加載器(Extension ClassLoader)
- Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現
- 派生於ClassLoader類
- 父類加載器為啟動類加載器
- 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。如果用戶創建的JAR放在此目錄下,也會自動由擴展類加載器加載
應用程序類加載器(也稱為系統類加載器,AppClassLoader)
- Java語言編寫,由sun.misc.LaunchersAppClassLoader實現
- 派生於ClassLoader類
- 父類加載器為擴展類加載器
- 它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫
- 該類加載是程序中默認的類加載器,一般來說,Java應用的類都是由它來完成加載
- 通過classLoader.getSystemclassLoader()方法可以獲取到該類加載器
- 隔離加載類(比如說我假設現在Spring框架,和RocketMQ有包名路徑完全一樣的類,類名也一樣,這個時候類就衝突了。不過一般的主流框架和中間件都會自定義類加載器,實現不同的框架,中間價之間是隔離的)
- 修改類加載的方式
- 擴展加載源(還可以考慮從數據庫中加載類,路由器等等不同的地方)
- 防止源碼泄漏(對位元組碼文件進行解密,自己用的時候通過自定義類加載器來對其進行解密)
- 開發人員可以通過繼承抽象類java.lang.ClassLoader類的方式,實現自己的類加載器,以滿足一些特殊的需求
- 在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類加載類,但是在JDK1.2之後已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findclass()方法中
- 在編寫自定義類加載器時,如果沒有太過於複雜的需求,可以直接繼承URIClassLoader類,這樣就可以避免自己去編寫findclass()方法及其獲取位元組碼流的方式,使自定義類加載器編寫更加簡潔。
public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(name); if (result == null) { throw new FileNotFoundException(); } else { //defineClass和findClass搭配使用 return defineClass(name, result, 0, result.length); } } catch (FileNotFoundException e) { e.printStackTrace(); } throw new ClassNotFoundException(name); } //自定義流的獲取方式 private byte[] getClassFromCustomPath(String name) { //從自定義路徑中加載指定類:細節略 //如果指定路徑的位元組碼文件進行了加密,則需要在此方法中進行解密操作。 return null; } public static void main(String[] args) { CustomClassLoader customClassLoader = new CustomClassLoader(); try { Class<?> clazz = Class.forName("One", true, customClassLoader); Object obj = clazz.newInstance(); System.out.println(obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
Java虛擬機對class文件採用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內存生成class對象。而且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式
- 如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行;
- 如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啟動類加載器;
- 如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式。
- 父類加載器一層一層往下分配任務,如果子類加載器能加載,則加載此類,如果將加載任務分配至系統類加載器也無法加載此類,則拋出異常
例如:
我們自己建立一個 java.lang.String 類,寫上 static 代碼塊
package java.lang; public class String { static{ System.out.println("自定義的String類的靜態代碼塊"); } }
在另外的程序中加載 String 類
public class StringTest { public static void main(String[] args) { java.lang.String str = new java.lang.String(); System.out.println("hello String"); } }
輸出結果:
hello String
並沒有打印自定義的String 類中的語句,所以系統加載的還是JDK 自帶的 String 類.
把剛剛的類改一下
package java.lang; public class String { static{ System.out.println("自定義的String類的靜態代碼塊"); } public static void main(String[] args) { System.out.println("hello String"); } }
由於雙親委派機制會一直找父類加載器,所以最後找到了Bootstrap ClassLoader(引導類加載器),Bootstrap ClassLoader找到的是 JDK 自帶的 String 類,在那個String類中並沒有相應的 main() 方法,所以就報了上面的錯誤。
- 避免類的重複加載
- 保護程序安全,防止核心API被隨意篡改
- 自定義String類時:在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載jdk自帶的文件(rt.jar包中java.lang.String.class),報錯信息說沒有main方法,就是因為加載的是rt.jar包中的String類。
- 這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制。
如果要判斷兩個class對象是否相同,在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:
- 類的完整類名必須一致,包括包名
- 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同
- 換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那麼這兩個類對象也是不相等的
對類加載器的引用
- JVM必須知道一個類型是由啟動加載器加載的還是由用戶類加載器加載的
- 如果一個類型是由用戶類加載器加載的,那麼JVM會將這個類加載器的一個引用作為類型信息的一部分保存在方法區中
- 當解析一個類型到另一個類型的引用的時候,JVM需要保證這兩個類型的類加載器是相同的