前言
本文將由淺及深,介紹Java
類載入的過程和原理,進一步對類載入器的進行原始碼分析,完成一個自定義的類載入器。
正文
(一). 類載入器是什麼
類載入器簡言之,就是用於把.class
檔案中的位元組碼資訊轉化為具體的java.lang.Class
物件的過程的工具。
具體過程:
- 在實際類載入過程中,
JVM
會將所有的.class
位元組碼檔案中的二進位制資料讀入記憶體中,匯入執行時資料區的方法區中。 - 當一個類首次被主動載入或被動載入時,類載入器會對此類執行類載入的流程 – 載入、連線(驗證、準備、解析)、初始化。
- 如果類載入成功,堆記憶體中會產生一個新的
Class
物件,Class
物件封裝了類在方法區內的資料結構。
Class
物件的建立過程描述:
(二). 類載入的過程
類載入的過程分為三個步驟(五個階段) :載入 -> 連線(驗證、準備、解析)-> 初始化。
載入、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段可以在初始化階段之後發生,也稱為動態繫結或晚期繫結。
類載入的過程描述:
1. 載入
載入:查詢並載入類的二進位制資料的過程。
載入的過程描述:
- 通過類的全限定名定位
.class
檔案,並獲取其二進位制位元組流。 - 把位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構。
- 在
Java
堆中生成一個此類的java.lang.Class
物件,作為方法區中這些資料的訪問入口。
2. 連線
連線:包括驗證、準備、解析三步。
a). 驗證
驗證:確保被載入的類的正確性。驗證是連線階段的第一步,用於確保Class
位元組流中的資訊是否符合虛擬機器的要求。
具體驗證形式:
- 檔案格式驗證:驗證位元組流是否符合
Class
檔案格式的規範;例如:是否以0xCAFEBABE
開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別。 - 後設資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比
javac
編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object
之外。 - 位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。
- 符號引用驗證:確保解析動作能正確執行。
b). 準備
準備:為類的靜態變數分配記憶體,並將其初始化為預設值。準備過程通常分配一個結構用來儲存類資訊,這個結構中包含了類中定義的成員變數,方法和介面資訊等。
具體行為:
- 這時候進行記憶體分配的僅包括類變數(
static
),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在Java
堆中。 - 這裡所設定的初始值通常情況下是資料型別預設的零值(如
0
、0L
、null
、false
等),而不是被在Java
程式碼中被顯式賦值。
c). 解析
解析:把類中對常量池內的符號引用轉換為直接引用。
解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符等7類符號引用進行。
3. 初始化
初始化:對類靜態變數賦予正確的初始值 (注意和連線時的解析過程區分開)。
初始化的目標
- 實現對宣告類靜態變數時指定的初始值的初始化;
- 實現對使用靜態程式碼塊設定的初始值的初始化。
初始化的步驟
- 如果此類沒被載入、連線,則先載入、連線此類;
- 如果此類的直接父類還未被初始化,則先初始化其直接父類;
- 如果類中有初始化語句,則按照順序依次執行初始化語句。
初始化的時機
- 建立類的例項(
new
關鍵字); java.lang.reflect
包中的方法(如:Class.forName(“xxx”)
);- 對類的靜態變數進行訪問或賦值;
- 訪問呼叫類的靜態方法;
- 初始化一個類的子類,父類本身也會被初始化;
- 作為程式的啟動入口,包含
main
方法(如:SpringBoot
入口類)。
(三). 類的主動引用和被動引用
主動引用
主動引用:在類載入階段,只執行載入、連線操作,不執行初始化操作。
主動引用的幾種形式
- 建立類的例項(
new
關鍵字); java.lang.reflect
包中的方法(如:Class.forName(“xxx”)
);- 對類的靜態變數進行訪問或賦值;
- 訪問呼叫類的靜態方法;
- 初始化一個類的子類,父類本身也會被初始化;
- 作為程式的啟動入口,包含
main
方法(如:SpringBoot
入口類)。
主動引用1 – main方法在初始類中
程式碼示例:
public class OptimisticReference0 {
static {
System.out.println(OptimisticReference0.class.getSimpleName() + " is referred!");
}
public static void main(String[] args) {
System.out.println();
}
}
複製程式碼
執行結果:
OptimisticReference0 is referred!
主動引用2 – 建立子類會觸發父類的初始化
程式碼示例:
public class OptimisticReference1 {
public static class Parent {
static {
System.out.println(Parent.class.getSimpleName() + " is referred!");
}
}
public static class Child extends Parent {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
new Child();
}
}
複製程式碼
執行結果:
Parent is referred!
Child is referred!
主動引用3 – 訪問一個類靜態變數
程式碼示例:
public class OptimisticReference2 {
public static class Child {
protected static String name;
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
name = "Child";
}
}
public static void main(String[] args) {
System.out.println(Child.name);
}
}
複製程式碼
執行結果:
Child is referred!
Child
主動引用4 – 對類的靜態變數進行賦值
程式碼示例:
public class OptimisticReference3 {
public static class Child {
protected static String name;
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
Child.name = "Child";
}
}
複製程式碼
執行結果:
Child is referred!
主動引用5 – 使用java.lang.reflect包提供的反射機制
程式碼示例:
public class OptimisticReference4 {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("org.ostenant.jdk8.learning.examples.reference.optimistic.Child");
}
}
複製程式碼
執行結果:
Child is referred!
被動引用
被動引用: 在類載入階段,會執行載入、連線和初始化操作。
被動引用的幾種形式:
- 通過子類引用父類的的靜態欄位,不會導致子類初始化;
- 定義類的陣列引用而不賦值,不會觸發此類的初始化;
- 訪問類定義的常量,不會觸發此類的初始化。
被動引用1 – 子類引用父類的的靜態欄位,不會導致子類初始化
程式碼示例:
public class NegativeReference0 {
public static class Parent {
public static String name = "Parent";
static {
System.out.println(Parent.class.getSimpleName() + " is referred!");
}
}
public static class Child extends Parent {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
System.out.println(Child.name);
}
}
複製程式碼
執行結果:
Parent is referred!
Parent
被動引用2 – 定義類的陣列引用而不賦值,不會觸發此類的初始化
程式碼示例:
public class NegativeReference1 {
public static class Child {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
Child[] childs = new Child[10];
}
}
複製程式碼
執行結果:
無輸出
被動引用3 – 訪問類定義的常量,不會觸發此類的初始化
示例程式碼:
public class NegativeReference2 {
public static class Child {
public static final String name = "Child";
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
System.out.println(Child.name);
}
}
複製程式碼
執行結果:
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 程式預設的類載入器 |
類載入器的特點
- 層級結構:Java裡的類裝載器被組織成了有父子關係的層級結構。Bootstrap類裝載器是所有裝載器的父親。
- 代理模式: 基於層級結構,類的代理可以在裝載器之間進行代理。當裝載器裝載一個類時,首先會檢查它在父裝載器中是否進行了裝載。如果上層裝載器已經裝載了這個類,這個類會被直接使用。反之,類裝載器會請求裝載這個類
- 可見性限制:一個子裝載器可以查詢父裝載器中的類,但是一個父裝載器不能查詢子裝載器裡的類。
- 不允許解除安裝:類裝載器可以裝載一個類但是不可以解除安裝它,不過可以刪除當前的類裝載器,然後建立一個新的類裝載器裝載。
類載入器的隔離問題
每個類裝載器都有一個自己的名稱空間用來儲存已裝載的類。當一個類裝載器裝載一個類時,它會通過儲存在名稱空間裡的類全侷限定名(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
- loadClass():通過指定類的全限定名稱,由類載入器檢測、裝載、建立並返回該類的
java.lang.Class
物件。
ClassLoader
通過loadClass()
方法實現了雙親委託機制,用於類的動態載入。
loadClass()
本身是一個遞迴向上呼叫的過程。
-
自底向上檢查類是否已載入
- 先通過
findLoadedClass()
方法從最底端類載入器開始檢查類是否已經載入。 - 如果已經載入,則根據
resolve
引數決定是否要執行連線過程,並返回Class
物件。 - 如果沒有載入,則通過
parent.loadClass()
委託其父類載入器執行相同的檢查操作(預設不做連線處理)。 - 直到頂級類載入器,即
parent
為空時,由findBootstrapClassOrNull()
方法嘗試到Bootstrap ClassLoader
中檢查目標類。
- 先通過
-
自頂向下嘗試載入類
- 如果仍然沒有找到目標類,則從
Bootstrap ClassLoader
開始,通過findClass()
方法嘗試到對應的類目錄下去載入目標類。 - 如果載入成功,則根據
resolve
引數決定是否要執行連線過程,並返回Class
物件。 - 如果載入失敗,則由其子類載入器嘗試載入,直到最底端類載入器也載入失敗,最終丟擲
ClassNotFoundException
。
- 如果仍然沒有找到目標類,則從
- findLoadedClass()
查詢當前類載入器的快取中是否已經載入目標類。
findLoadedClass()
實際呼叫了底層的native
方法findLoadedClass0()
。
- findBootstrapClassOrNull()
查詢最頂端
Bootstrap
類載入器的是否已經載入目標類。同樣,findBootstrapClassOrNull()
實際呼叫了底層的native
方法findBootstrapClass()
。
- findClass()
ClassLoader
是java.lang
包下的抽象類,也是所有類載入器(除了Bootstrap
)的基類,findClass()
是ClassLoader
對子類提供的載入目標類的抽象方法。注意:
Bootstrap ClassLoader
並不屬於JVM
的層次,它不遵守ClassLoader
的載入規則,Bootstrap classLoader
並沒有子類。
- defineClass()
defineClass()
是ClassLoader
向子類提供的方法,它可以將.class
檔案的二進位制資料轉換為合法的java.lang.Class
物件。
(六). 類的動態載入
類的幾種載入方式
- 通過命令列啟動時由
JVM
初始化載入; - 通過
Class.forName()
方法動態載入; - 通過
ClassLoader.loadClass()
方法動態載入。
Class.forName()和ClassLoader.loadClass()
- Class.forName():把類的
.class
檔案載入到JVM
中,對類進行解釋的同時執行類中的static
靜態程式碼塊; - ClassLoader.loadClass():只是把.class檔案載入到
JVM
中,不會執行static
程式碼塊中的內容,只有在newInstance
才會去執行。
(七). 物件的初始化
物件的初始化順序
靜態變數/靜態程式碼塊 -> 普通程式碼塊 -> 建構函式
- 父類靜態變數和靜態程式碼塊(先宣告的先執行);
- 子類靜態變數和靜態程式碼塊(先宣告的先執行);
- 父類普通成員變數和普通程式碼塊(先宣告的先執行);
- 父類的建構函式;
- 子類普通成員變數和普通程式碼塊(先宣告的先執行);
- 子類的建構函式。
物件的初始化示例
Parent.java
Children.java
Tester.java
測試結果:
測試結果表明:JVM
在建立物件時,遵守以上物件的初始化順序。
(八). 自定義類載入器
編寫自己的類載入器
在原始碼分析階段,我們已經解讀了如何實現自定義類載入器,現在我們開始懟自己的類載入器。
Step 1:定義待載入的目標類
Parent.java
和Children.java
。
Parent.java
package org.ostenant.jdk8.learning.examples.classloader.custom;
public class Parent {
protected static String CLASS_NAME;
protected static String CLASS_LOADER_NAME;
protected String instanceID;
// 1.先執行靜態變數和靜態程式碼塊(只在類載入期間執行一次)
static {
CLASS_NAME = Parent.class.getName();
CLASS_LOADER_NAME = Parent.class.getClassLoader().toString();
System.out.println("Step a: " + CLASS_NAME + " is loaded by " + CLASS_LOADER_NAME);
}
// 2.然後執行變數和普通程式碼塊(每次建立例項都會執行)
{
instanceID = this.toString();
System.out.println("Step c: Parent instance is created: " + CLASS_LOADER_NAME + " -> " + instanceID);
}
// 3.然後執行構造方法
public Parent() {
System.out.println("Step d: Parent instance:" + instanceID + ", constructor is invoked");
}
public void say() {
System.out.println("My first class loader...");
}
}
複製程式碼
Children.java
package org.ostenant.jdk8.learning.examples.classloader.custom;
public class Children extends Parent {
static {
CLASS_NAME = Children.class.getName();
CLASS_LOADER_NAME = Children.class.getClassLoader().toString();
System.out.println("Step b: " + CLASS_NAME + " is loaded by " + CLASS_LOADER_NAME);
}
{
instanceID = this.toString();
System.out.println("Step e: Children instance is created: " + CLASS_LOADER_NAME + " -> " + instanceID);
}
public Children() {
System.out.println("Step f: Children instance:" + instanceID + ", constructor is invoked");
}
public void say() {
System.out.println("My first class loader...");
}
}
複製程式碼
Step 2:實現自定義類載入器
CustomClassLoader
CustomClassLoader.java
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name); // 可省略
if (c == null) {
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException();
}
return defineClass(name, data, 0, data.length);
}
return null;
}
protected byte[] loadClassData(String name) {
try {
// package -> file folder
name = name.replace(".", "//");
FileInputStream fis = new FileInputStream(new File(classPath + "//" + name + ".class"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = -1;
byte[] b = new byte[2048];
while ((len = fis.read(b)) != -1) {
baos.write(b, 0, len);
}
fis.close();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
複製程式碼
Step 3:測試類載入器的載入過程
CustomerClassLoaderTester.java
- 測試程式啟動時,逐一拷貝並載入待載入的目標類原始檔。
private static final String CHILDREN_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Children.java";
private static final String PARENT_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Parent.java";
private static final List<String> SOURCE_CODE = Arrays.asList(CHILDREN_SOURCE_CODE_NAME, PARENT_SOURCE_CODE_NAME);
static {
SOURCE_CODE.stream().map(path -> new File(path))
// 路徑轉檔案物件
.filter(f -> !f.isDirectory())
// 檔案遍歷
.forEach(f -> {
// 拷貝後原始碼
File targetFile = copySourceFile(f);
// 編譯原始碼
compileSourceFile(targetFile);
});
}
複製程式碼
- 拷貝單一原始檔到自定義類載入器的類載入目錄。
protected static File copySourceFile(File f) {
BufferedReader reader = null;
BufferedWriter writer = null;
try {
reader = new BufferedReader(new FileReader(f));
// package ...;
String firstLine = reader.readLine();
StringTokenizer tokenizer = new StringTokenizer(firstLine, " ");
String packageName = "";
while (tokenizer.hasMoreElements()) {
String e = tokenizer.nextToken();
if (e.contains("package")) {
continue;
} else {
packageName = e.trim().substring(0, e.trim().length() - 1);
}
}
// package -> path
String packagePath = packageName.replace(".", "//");
// java file path
String targetFileLocation = TARGET_CODE_LOCALTION + "//" + packagePath + "//";
String sourceFilePath = f.getPath();
String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf("\") + 1);
File targetFile = new File(targetFileLocation, fileName);
File targetFileLocationDir = new File(targetFileLocation);
if (!targetFileLocationDir.exists()) {
targetFileLocationDir.mkdirs();
}
// writer
writer = new BufferedWriter(new FileWriter(targetFile));
// 寫入第一行
writer.write(firstLine);
writer.newLine();
writer.newLine();
String input = "";
while ((input = reader.readLine()) != null) {
writer.write(input);
writer.newLine();
}
return targetFile;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
reader.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
複製程式碼
- 對拷貝後的
.java
原始檔執行手動編譯,在同級目錄下生成.class
檔案。
protected static void compileSourceFile(File f) {
try {
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager standardFileManager = javaCompiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> javaFileObjects = standardFileManager.getJavaFileObjects(f);
// 執行編譯任務
CompilationTask task = javaCompiler.getTask(null, standardFileManager, null, null, null, javaFileObjects);
task.call();
standardFileManager.close();
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
- 通過自定義類載入器載入
Children
的java.lang.Class<?>
物件,然後用反射機制建立Children
的例項物件。
@Test
public void test() throws Exception {
// 建立自定義類載入器
CustomClassLoader classLoader = new CustomClassLoader(TARGET_CODE_LOCALTION); // E://myclassloader//classpath
// 動態載入class檔案到記憶體中(無連線)
Class<?> c = classLoader.loadClass("org.ostenant.jdk8.learning.examples.classloader.custom.Children");
// 通過反射拿到所有的方法
Method[] declaredMethods = c.getDeclaredMethods();
for (Method method : declaredMethods) {
if ("say".equals(method.getName())) {
// 通過反射拿到children物件
Object children = c.newInstance();
// 呼叫children的say()方法
method.invoke(children);
break;
}
}
}
複製程式碼
測試編寫的類載入器
(一). 測試場景一
- 保留
static
程式碼塊,把目標類Children.java
和Parent.java
拷貝到類載入的目錄,然後進行手動編譯。 - 保留測試專案目錄中的目標類
Children.java
和Parent.java
。
測試結果輸出:
測試結果分析:
我們成功建立了
Children
物件,並通過反射呼叫了它的say()
方法。
然而檢視控制檯日誌,可以發現類載入使用的仍然是AppClassLoader
,CustomClassLoader
並沒有生效。
檢視CustomClassLoader
的類載入目錄:
類目錄下有我們拷貝並編譯的
Parent
和Chidren
檔案。
分析原因:
由於專案空間中的
Parent.java
和Children.java
,在拷貝後並沒有移除。導致AppClassLoader
優先在其Classpath
下面找到併成功載入了目標類。
(二). 測試場景二
- 註釋掉
static
程式碼塊(類目錄下有已編譯的目標類.class
檔案)。 - 移除測試專案目錄中的目標類
Children.java
和Parent.java
。
測試結果輸出:
測試結果分析:
我們成功通過自定義類載入器載入了目標類。建立了
Children
物件,並通過反射呼叫了它的say()
方法。
至此,我們自己的一個簡單的類載入器就完成了!
參考書籍
周志明,深入理解Java虛擬機器:JVM高階特性與最佳實踐,機械工業出版社
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。