本文將由淺及深,介紹Java
類加載的過程和原理,進一步對類加載器的進行源碼分析,完成一個自定義的類加載器。java
類加載器簡言之,就是用於把.class
文件中的字節碼信息轉化爲具體的java.lang.Class
對象的過程的工具。編程
具體過程:後端
JVM
會將全部的.class
字節碼文件中的二進制數據讀入內存中,導入運行時數據區的方法區中。Class
對象,Class
對象封裝了類在方法區內的數據結構。Class
對象的建立過程描述:數組
類加載的過程分爲三個步驟(五個階段) :加載 -> 鏈接(驗證、準備、解析)-> 初始化。緩存
加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段能夠在初始化階段以後發生,也稱爲動態綁定或晚期綁定。數據結構
加載:查找並加載類的二進制數據的過程。架構
.class
文件,並獲取其二進制字節流。Java
堆中生成一個此類的java.lang.Class
對象,做爲方法區中這些數據的訪問入口。鏈接:包括驗證、準備、解析三步。框架
驗證:確保被加載的類的正確性。驗證是鏈接階段的第一步,用於確保Class
字節流中的信息是否符合虛擬機的要求。異步
Class
文件格式的規範;例如:是否以0xCAFEBABE
開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。javac
編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object
以外。準備:爲類的靜態變量分配內存,並將其初始化爲默認值。準備過程一般分配一個結構用來存儲類信息,這個結構中包含了類中定義的成員變量,方法和接口信息等。
static
),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java
堆中。0
、0L
、null
、false
等),而不是被在Java
代碼中被顯式賦值。解析:把類中對常量池內的符號引用轉換爲直接引用。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符等7類符號引用進行。
初始化:對類靜態變量賦予正確的初始值 (注意和鏈接時的解析過程區分開)。
new
關鍵字);java.lang.reflect
包中的方法(如:Class.forName(「xxx」)
);main
方法(如:SpringBoot
入口類)。主動引用:在類加載階段,只執行加載、鏈接操做,不執行初始化操做。
new
關鍵字);java.lang.reflect
包中的方法(如:Class.forName(「xxx」)
);main
方法(如:SpringBoot
入口類)。代碼示例:
1 |
public class OptimisticReference0 { |
運行結果:
OptimisticReference0 is referred!
代碼示例:
1 |
public class OptimisticReference1 { |
運行結果:
Parent is referred!
Child is referred!
代碼示例:
1 |
public class OptimisticReference2 { |
運行結果:
Child is referred!
Child
代碼示例:
1 |
public class OptimisticReference3 { |
運行結果:
Child is referred!
代碼示例:
1 |
public class OptimisticReference4 { |
運行結果:
Child is referred!
被動引用: 在類加載階段,會執行加載、鏈接和初始化操做。
被動引用的幾種形式:
代碼示例:
1 |
public class NegativeReference0 { |
運行結果:
Parent is referred!
Parent
代碼示例:
1 |
public class NegativeReference1 { |
運行結果:
無輸出
示例代碼:
1 |
public class NegativeReference2 { |
運行結果:
Child
類加載器:類加載器負責加載程序中的類型(類和接口),並賦予惟一的名字予以標識。
Bootstrap Classloader
是在Java
虛擬機啓動後初始化的。Bootstrap Classloader
負責加載 ExtClassLoader
,而且將 ExtClassLoader
的父加載器設置爲 Bootstrap Classloader
Bootstrap Classloader
加載完 ExtClassLoader
後,就會加載 AppClassLoader
,而且將 AppClassLoader
的父加載器指定爲 ExtClassLoader
。Class Loader | 實現方式 | 具體實現類 | 負責加載的目標 |
---|---|---|---|
Bootstrap Loader | C++ | 由C++實現 | %JAVA_HOME%/jre/lib/rt.jar 以及-Xbootclasspath 參數指定的路徑以及中的類庫 |
Extension ClassLoader | Java | sun.misc.Launcher$ExtClassLoader | %JAVA_HOME%/jre/lib/ext 路徑下以及java.ext.dirs 系統變量指定的路徑中類庫 |
Application ClassLoader | Java | sun.misc.Launcher$AppClassLoader | Classpath 以及-classpath 、-cp 指定目錄所指定的位置的類或者是jar 文檔,它也是Java 程序默認的類加載器 |
每一個類裝載器都有一個本身的命名空間用來保存已裝載的類。當一個類裝載器裝載一個類時,它會經過保存在命名空間裏的類全侷限定名(Fully Qualified Class Name
) 進行搜索來檢測這個類是否已經被加載了。
JVM
及 Dalvik
對類惟一的識別是 ClassLoader id
+ PackageName
+ ClassName
,因此一個運行程序中是有可能存在兩個包名和類名徹底一致的類的。而且若是這兩個類不是由一個 ClassLoader
加載,是沒法將一個類的實例強轉爲另一個類的,這就是 ClassLoader
隔離性。
爲了解決類加載器的隔離問題,JVM
引入了雙親委託機制。
核心思想:其一,自底向上檢查類是否已加載;其二,自頂向下嘗試加載類。
AppClassLoader
加載一個class
時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader
去完成。ExtClassLoader
加載一個class
時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader
去完成。BootStrapClassLoader
加載失敗(例如在%JAVA_HOME%/jre/lib
裏未查找到該class
),會使用ExtClassLoader
來嘗試加載;ExtClassLoader
也加載失敗,則會使用AppClassLoader
來加載,若是AppClassLoader
也加載失敗,則會報出異常ClassNotFoundException
。ClassLoader.class
java.lang.Class
對象。
ClassLoader
經過loadClass()
方法實現了雙親委託機制,用於類的動態加載。
loadClass()
自己是一個遞歸向上調用的過程。
自底向上檢查類是否已加載
findLoadedClass()
方法從最底端類加載器開始檢查類是否已經加載。resolve
參數決定是否要執行鏈接過程,並返回Class
對象。parent.loadClass()
委託其父類加載器執行相同的檢查操做(默認不作鏈接處理)。parent
爲空時,由findBootstrapClassOrNull()
方法嘗試到Bootstrap ClassLoader
中檢查目標類。自頂向下嘗試加載類
Bootstrap ClassLoader
開始,經過findClass()
方法嘗試到對應的類目錄下去加載目標類。resolve
參數決定是否要執行鏈接過程,並返回Class
對象。ClassNotFoundException
。查找最頂端
Bootstrap
類加載器的是否已經加載目標類。一樣,findBootstrapClassOrNull()
實際調用了底層的native
方法findBootstrapClass()
。
ClassLoader
是java.lang
包下的抽象類,也是全部類加載器(除了Bootstrap
)的基類,findClass()
是ClassLoader
對子類提供的加載目標類的抽象方法。注意:
Bootstrap ClassLoader
並不屬於JVM
的層次,它不遵照ClassLoader
的加載規則,Bootstrap classLoader
並無子類。
JVM
初始化加載;Class.forName()
方法動態加載;ClassLoader.loadClass()
方法動態加載。.class
文件加載到JVM
中,對類進行解釋的同時執行類中的static
靜態代碼塊;JVM
中,不會執行static
代碼塊中的內容,只有在newInstance
纔會去執行。靜態變量/靜態代碼塊 -> 普通代碼塊 -> 構造函數
測試結果代表:JVM
在建立對象時,遵照以上對象的初始化順序。
在源碼分析階段,咱們已經解讀了如何實現自定義類加載器,如今咱們開始懟本身的類加載器。
Step 1:定義待加載的目標類
Parent.java
和Children.java
。
Parent.java
1 |
package org.ostenant.jdk8.learning.examples.classloader.custom; |
Children.java
1 |
package org.ostenant.jdk8.learning.examples.classloader.custom; |
Step 2:實現自定義類加載器
CustomClassLoader
CustomClassLoader.java
1 |
public class CustomClassLoader extends ClassLoader { |
Step 3:測試類加載器的加載過程
CustomerClassLoaderTester.java
測試程序啓動時,逐一拷貝並加載待加載的目標類源文件。
1 |
private static final String CHILDREN_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Children.java"; |
拷貝單一源文件到自定義類加載器的類加載目錄。
1 |
protected static File copySourceFile(File f) { |
對拷貝後的.java
源文件執行手動編譯,在同級目錄下生成.class
文件。
1 |
protected static void compileSourceFile(File f) { |
經過自定義類加載器加載Children
的java.lang.Class<?>
對象,而後用反射機制建立Children
的實例對象。
1 |
|
static
代碼塊,把目標類Children.java
和Parent.java
拷貝到類加載的目錄,而後進行手動編譯。Children.java
和Parent.java
。測試結果分析:
咱們成功建立了
Children
對象,並經過反射調用了它的say()
方法。
然而查看控制檯日誌,能夠發現類加載使用的仍然是AppClassLoader
,CustomClassLoader
並無生效。
類目錄下有咱們拷貝並編譯的
Parent
和Chidren
文件。
分析緣由:
因爲項目空間中的
Parent.java
和Children.java
,在拷貝後並無移除。致使AppClassLoader
優先在其Classpath
下面找到併成功加載了目標類。
咱們成功經過自定義類加載器加載了目標類。建立了
Children
對象,並經過反射調用了它的say()
方法。
至此,咱們本身的一個簡單的類加載器就完成了!
周志明,深刻理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。