本文將由淺及深,介紹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
入口類)。代碼示例:
public class OptimisticReference0 {
static {
System.out.println(OptimisticReference0.class.getSimpleName() + " is referred!");
}
public static void main(String[] args) {
System.out.println();
}
}
複製代碼
運行結果:
OptimisticReference0 is referred!
代碼示例:
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!
代碼示例:
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
代碼示例:
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!
代碼示例:
public class OptimisticReference4 {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("org.ostenant.jdk8.learning.examples.reference.optimistic.Child");
}
}
複製代碼
運行結果:
Child is referred!
被動引用: 在類加載階段,會執行加載、鏈接和初始化操做。
被動引用的幾種形式:
代碼示例:
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
代碼示例:
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];
}
}
複製代碼
運行結果:
無輸出
示例代碼:
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 程序默認的類加載器 |
每一個類裝載器都有一個本身的命名空間用來保存已裝載的類。當一個類裝載器裝載一個類時,它會經過保存在命名空間裏的類全侷限定名(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
。查找當前類加載器的緩存中是否已經加載目標類。
findLoadedClass()
實際調用了底層的native
方法findLoadedClass0()
。
查找最頂端
Bootstrap
類加載器的是否已經加載目標類。一樣,findBootstrapClassOrNull()
實際調用了底層的native
方法findBootstrapClass()
。
ClassLoader
是java.lang
包下的抽象類,也是全部類加載器(除了Bootstrap
)的基類,findClass()
是ClassLoader
對子類提供的加載目標類的抽象方法。注意:
Bootstrap ClassLoader
並不屬於JVM
的層次,它不遵照ClassLoader
的加載規則,Bootstrap classLoader
並無子類。
defineClass()
是ClassLoader
向子類提供的方法,它能夠將.class
文件的二進制數據轉換爲合法的java.lang.Class
對象。
JVM
初始化加載;Class.forName()
方法動態加載;ClassLoader.loadClass()
方法動態加載。.class
文件加載到JVM
中,對類進行解釋的同時執行類中的static
靜態代碼塊;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高級特性與最佳實踐,機械工業出版社
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。