重新對Java的類加載器的學習方式
Java 類加載器和JVM 類加載器是同一體系中的不同概念。
Java 類加載器是一個更高抽象層面的概念,可以實現(xiàn)自定義加載器;而 JVM 類加載器是 JVM 的實現(xiàn)部分,負責實際的字節(jié)碼加載。
類加載器:負責將.class文件加載到內存中,并為之生成對應的Class對象(是JVM執(zhí)行類加載機制的前提)
1、介紹
1.1、簡介
在Java中,類加載器(ClassLoader)是Java虛擬機(JVM)用來加載類的核心組件。
它負責將Java字節(jié)碼文件(.class文件)動態(tài)加載到內存中,并將其轉化為JVM可以執(zhí)行的類對象。類加載器是Java運行時系統(tǒng)的一部分,它支持Java的動態(tài)特性,使得Java程序可以在運行時加載類和接口。
如下圖所示:
1.2、符號引用和直接引用
關于符號引用和直接引用的介紹這里先進行一個理解,下面在類加載器執(zhí)行過程的連接階段,有個解析的過程需要聯(lián)系到這里的知識。
- 符號引用:指的是一個字符串,該字符串表示一個類、字段或方法的名稱,這個名稱由 JVM 在運行時解析。
- 直接引用:指的是內存中對象的具體地址或偏移量,它用于直接訪問該字段或調用該方法。
1、符號引用
符號引用通常在 Java 字節(jié)碼(class
文件)中以字符串的形式出現(xiàn)。它是類與類之間一種相對的、靈活的引用方式。這種引用方式在字節(jié)碼編譯時就已經確定,但實際內存地址在運行時才會分配。
示例:符號引用的特點
考慮一下這段代碼:
class Example { void hello() { System.out.println("Hello, World!"); } }
在編譯成字節(jié)碼后,hello
方法的符號引用會包含:
- 方法名:
hello
- 方法描述符:
()V
(意味著無參數(shù)且沒有返回值)
在字節(jié)碼中的常量池部分,可以看到以下條目(這里是簡化的表示):
1. Class: 'Example' 2. Method: 'hello' with descriptor '()V'
這個符號引用不會包含任何內存地址或具體實現(xiàn)細節(jié)。
2、直接引用
當 JVM 運行時解析符號引用時,它會將符號引用轉換為直接引用。直接引用是指向內存中對象或方法入口的確切地址,這樣 JVM 就可以直接訪問它們。
示例:直接引用的獲取過程
下面的例子展示了符號引用到直接引用的過程。
public class Main { public static void main(String[] args) { Example example = new Example(); // 創(chuàng)建 Example 對象 example.hello(); // 調用 hello 方法 } } class Example { void hello() { System.out.println("Hello, World!"); } }
在這個代碼中:
example.hello()
是調用hello
方法。在編譯時,hello
方法的引用是符號引用。- 當 JVM 到達這行代碼時,它首先會解析
hello
的符號引用。
3、符號轉直接的過程
1.指向類定義:
在符號引用查找過程中,JVM 會查找并確認 Example
類的符號引用,即它的名稱 Example
。
2.處理方法:
一旦 Example
類被確認可用,JVM 將查找 hello
方法的符號引用。當它找到這個符號引用時,它會定位到方法在內存中的地址(也就是直接引用)。
這個直接引用通常是方法在內存中相對于類對象的偏移。
3.執(zhí)行調用:
最后,JVM 使用這個直接引用來調用 hello
方法,從而直接在內存中找到并執(zhí)行方法的字節(jié)碼。
2、加載流程
Java是一個動態(tài)語言,這意味著類在程序運行時被加載,而不是在編譯時完成加載。類加載器的主要任務就是將類的字節(jié)碼文件從文件系統(tǒng)或網絡等資源加載到內存中。
具體而言,類加載器的職責包括:
- 加載類:將Java字節(jié)碼文件讀取到內存,并轉換為
Class
對象。 - 鏈接類:將類的二進制數(shù)據合并到JVM運行時環(huán)境中。這一步包括驗證、準備和解析。
- 初始化類:執(zhí)行類的靜態(tài)初始化塊和靜態(tài)變量的初始化。
關于上述各個階段的主要流程下面進行了詳細的介紹。
由上圖可知:
ClassLoader在整個裝載階段,只能影響到類的加載,而無法通過ClassLoader去改變類的鏈接和初始化行為。
整個執(zhí)行過程可以分為三大步:加載、連接、初始化。
1.加載:將字節(jié)碼文件通過IO流讀取到JVM的方法區(qū),并同時在堆中生成Class對像。
2.鏈接:
- 驗證:校驗字節(jié)碼文件的正確性。
- 準備:為類的靜態(tài)變量分配內存,并初始化為默認值;對于final static修飾的變量,在編譯時就已經分配好內存了。
- 解析:將類中的符號引用轉換為直接引用。
注意:如果類加載后,未通過驗證,則不能被使用。
3.初始化:對類的靜態(tài)變量和靜態(tài)代碼塊初始化為指定的值,執(zhí)行靜態(tài)代碼。
- ClassLoader是Java的核心組件,所有的Class都是由ClassLoader進行加載的。
- ClassLoader是否可以運行,則由Execution Engine決定。
- ClassLoader負責通過各種方式將Class信息的二進制數(shù)據流讀入JVM內部,轉換為一個與目標類對應的java.lang.Class對象實例,然后交給Java虛擬機進行鏈接、初始化等操作。
3、類加載的分類
分為顯式加載 vs 隱式加載(即JVM加載class文件到內存的方式)
3.1、顯示加載:
在代碼中顯示調用ClassLoader加載class對象。
實現(xiàn)方式
- 1.Class.forName(name)
- 2.this.getClass().
- 3.getClassLoader().loadClass() 。
加載class對象。
3.2、隱式加載:
通過虛擬機自動加載到內存中,是不直接在代碼中調用ClassLoader的方法加載class對象,類在被引用(如調用靜態(tài)方法或訪問靜態(tài)字段)時自動加載。
如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象,此時額外引用的類將通過JVM自動加載到內存中。
代碼示例:
// 隱式加載 User user = new User(); // 顯式加載,并初始化 Class clazz = Class.forName("com.test.java.User"); // 顯式加載,但不初始化 ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");
心得:
- 隱式加載:為開發(fā)者簡化了加載過程,不需要顯式調用,通常在程序中不易察覺。
- 顯式加載:可用于動態(tài)加載類,靈活控制加載時機。
4、命名空間
命名空間指的是在一定范圍內,標識符(如類名、變量名等)被唯一綁定到一個特定的實體。對于類加載器而言,命名空間是指每個類加載器有其各自的發(fā)現(xiàn)和加載類的范圍,它負責自己加載的類及其依賴關系。
4.1、類加載器和命名空間的關系
1.隔離性
每個類加載器都有一個獨立的命名空間。相同類名的類可以在不同的類加載器中存在,而彼此不會干擾。
例如,一個 JAR 中的 com.example.MyClass
可以通過不同的類加載器各自加載,而不會發(fā)生沖突。
2.父類優(yōu)先原則
當一個類加載器加載類時,它會首先將加載請求委托給它的父類加載器。這種機制確保了核心類庫得以優(yōu)先加載,從而避免了相同名稱的類在不同上下文中出現(xiàn)。
4.2、示例
以下是一個簡單的示例,展示不同類加載器之間的命名空間如何影響類的加載。
MyClass.java:
package com.example; public class MyClass { static { System.out.println("MyClass loaded!"); } }
CustomClassLoader.java:
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String filePath = name.replace('.', File.separatorChar) + ".class"; try (FileInputStream fis = new FileInputStream(new File(filePath))) { byte[] b = new byte[fis.available()]; fis.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException("Class not found: " + name, e); } } }
Main.java:
public class Main { public static void main(String[] args) { try { CustomClassLoader loader1 = new CustomClassLoader(); CustomClassLoader loader2 = new CustomClassLoader(); // 使用兩個自定義類加載器加載同一類 Class<?> class1 = loader1.loadClass("com.example.MyClass"); Class<?> class2 = loader2.loadClass("com.example.MyClass"); // 檢查兩個類加載器加載的類是否相同 System.out.println("Are class1 and class2 the same? " + (class1 == class2)); // 創(chuàng)建實例 Object instance1 = class1.getDeclaredConstructor().newInstance(); Object instance2 = class2.getDeclaredConstructor().newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
當運行 Main.java
時,輸出可能會是:由于加載器不同。
MyClass loaded! MyClass loaded! Are class1 and class2 the same? false
解釋
- 雙重加載:由于我們使用了兩個不同的自定義類加載器 (
loader1
和loader2
) 來加載同一個類com.example.MyClass
,因此它們在內存中生成了兩個不同的Class
實例。 - 命名空間隔離:
class1
和class2
雖然指向同一個類的符號引用,但由于在不同的類加載器中加載,它們擁有獨立的命名空間,因此class1 == class2
結果為false
。
總結
- 命名空間:為每個類加載器創(chuàng)建了一個獨立的命名空間,使得相同名稱的類可以共存于不同的上下文中。
- 父類優(yōu)先原則:用于提升類加載的安全性,優(yōu)先通過父類加載器查找類。
- 自定義類加載器:可以實現(xiàn)輕松管理類加載過程,提供更多靈活性和控制權。
5、類加載器的分類
JVM支持兩種類型的類加載器,分別為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(C z ClassLoader)。
- Bootstrap ClassLoader(啟動類加載器):
- Extension ClassLoader(擴展類加載器):
- Application ClassLoader(系統(tǒng)類加載器):
自定義類加載器:擴展 java.lang.ClassLoader
代碼示例:
public class ClassTestLoader extends ClassLoader{ public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = new ClassTestLoader(); Class<?> clazz = classLoader.loadClass("com.ali.sls.test.Counter"); System.out.println("class loader:=="+ clazz.getClassLoader()); System.out.println("class loader:=="+ clazz.getClassLoader().getParent()); System.out.println("class loader:=="+ clazz.getClassLoader().getParent().getParent()); } } class loader:==sun.misc.Launcher$AppClassLoader@18b4aac2 class loader:==sun.misc.Launcher$ExtClassLoader@6433a2 class loader:==null
6、雙親委派
Java的類加載機制采用了雙親委派模型(Parent Delegation Model)。該模型的核心思想是:當一個類加載器試圖加載某個類時,它會先將這個請求委托給父類加載器,而不是自己直接加載。只有當父類加載器無法找到該類時,才由當前類加載器嘗試加載。
6.1、緩存機制
每個類加載器(包括父類加載器)都會維護一個緩存,用于存儲已經加載過的類。當類加載器收到加載請求時,會首先檢查緩存中是否已經加載過該類。如果已經加載過,則直接返回緩存的類,而不會重新加載。
- 子類加載器加載的類:
子類加載器加載的類會存儲在子類加載器的緩存中,父類加載器無法訪問子類加載器的緩存。
- 父類加載器加載的類:
父類加載器加載的類會存儲在父類加載器的緩存中,子類加載器可以通過雙親委派機制訪問父類加載器的緩存。
因此,如果子類加載器已經加載了某個類,父類加載器不會再次加載該類,因為父類加載器無法感知子類加載器的緩存。
6.2、類的唯一性
在JVM中,類的唯一性是由 類的全限定名 + 類加載器 共同決定的。即使兩個類加載器加載了同一個類的字節(jié)碼,JVM也會將它們視為不同的類。
如果子類加載器加載了一個類,父類加載器再次嘗試加載同一個類,JVM會認為這是兩個不同的類(因為類加載器不同)。這可能導致 類沖突 或 類型轉換異常,因為JVM認為這兩個類是獨立的。
6.3、工作流程:
類加載器接收到加載請求時,首先將請求委派給父類加載器。
如果父類加載器能找到該類,則加載成功;如果父類加載器無法加載該類,則由當前類加載器加載。這種機制確保了Java核心類庫不會被用戶自定義的類加載器替代或覆蓋。
6.4、如何打破
1:自定義類加載器
我們將創(chuàng)建一個自定義的類加載器,它直接加載某個特定包中的類,而不經過父加載器。
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> loadClass(String name) throws ClassNotFoundException { // 檢查是否需要執(zhí)行自定義加載 if (name.startsWith("com.example")) { // 這里實際上不調用父類加載器 return findClass(name); } // 否則,使用默認的父類加載器 return super.loadClass(name); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String filePath = classPath + File.separator + name.replace('.', File.separatorChar) + ".class"; try (FileInputStream fis = new FileInputStream(filePath)) { byte[] b = new byte[fis.available()]; fis.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException("Class not found: " + name, e); } } }
解釋:
- 在
loadClass
方法中,我們直接處理以com.example
開頭的類,調用findClass
來加載。而對于其他類,則調用super.loadClass(name)
,這表明默認的父類加載器將處理它。
注意:確保 com/example/MyClass.class
文件在 "path/to/classes"
目錄中。
2.使用 Thread
的上下文類加載器
在某些情況下,也可以通過設置當前線程的上下文類加載器(context class loader)來打破雙親委派。
public class ContextClassLoaderExample { public static void main(String[] args) { // 設置自定義類加載器為當前線程的上下文類加載器 Thread.currentThread().setContextClassLoader(new MyClassLoader("path/to/classes")); // 然后通過上下文類加載器加載類 ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); try { Class<?> myClass = contextClassLoader.loadClass("com.example.MyClass"); // 直接調用上下文類加載器 System.out.println("Loaded class: " + myClass.getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
7、類的卸載
Java 的 GC(垃圾回收)會在類引用不存在時進行類的卸載。只有使用 ClassLoader
加載的類,如果其 ClassLoader 被卸載,該類才會被卸載。
類的卸載并不是一個強制性的操作,只有在特定條件下才會發(fā)生。
7.1、卸載條件
類的卸載發(fā)生在以下情況下:
1.類加載器被垃圾回收
當沒有任何引用指向某個類加載器時,該類加載器及其所加載的類有可能被卸載。JVM 可以回收類加載器,并同時卸載它加載的所有類。
2.類及其類加載器均無法到達
類和類加載器不僅沒有引用,而且它們在常量池、棧幀等處也不再被引用時,可以進行卸載。
7.2、觸發(fā)條件
雖然部分類可以在 Java 程序運行時被卸載,但是的確沒有顯式的方法去卸載類,整個卸載過程是由 JVM 的垃圾回收器自動處理。
以下是一些觸發(fā)類卸載的條件。
1.動態(tài)類加載:
當使用自定義類加載器動態(tài)加載類時,如果不再有引用指向這個類和類加載器,它們會被視為垃圾對象,從而可能被回收。
2.Classpath 變化:
如果應用程序在運行時改變了類路徑(比如加載新版本的同名類),舊的類及其加載器可能被卸載。
以下是一個類卸載的簡單示例:
public class ClassUnloadingExample { public static void main(String[] args) { CustomClassLoader classLoader = new CustomClassLoader(); try { Class<?> clazz1 = classLoader.loadClass("com.example.MyClass"); // 使用 clazz1 Object instance = clazz1.getDeclaredConstructor().newInstance(); System.out.println("Class Loaded: " + clazz1.getName()); // 設置 classLoader 為 null,解除引用 classLoader = null; // 觸發(fā)垃圾回收 System.gc(); Thread.sleep(1000); // 確保 GC 有足夠時間運行 // 這里將不會再有引用指向這個類,可能被卸載 System.out.println("Unloading classes..."); } catch (Exception e) { e.printStackTrace(); } } } // 自定義類加載器 class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 加載類的邏輯 // 假設類文件框架完整 return super.findClass(name); } }
8、類加載異常處理
在類加載過程中可能會遇到的異常主要有:
ClassNotFoundException
: 當請求的類不存在時拋出。NoClassDefFoundError
: 類存在但不再可用,通常是因為 class 文件被刪除或 JVM 啟動時未找到。UnsupportedClassVersionError
: 由于 Java 版本不兼容導致的錯誤。
總結
通過上述文章的介紹,希望可以幫助開發(fā)者在項目日常中更加清晰了解java類的加載機制原理。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Spring Boot 使用 Swagger 構建 RestAPI 接口文檔
這篇文章主要介紹了Spring Boot 使用 Swagger 構建 RestAPI 接口文檔,幫助大家更好的理解和使用Spring Boot框架,感興趣的朋友可以了解下2020-10-10SpringBoot+Quartz實現(xiàn)動態(tài)定時任務
這篇文章主要為大家詳細介紹了springBoot+Quartz實現(xiàn)動態(tài)定時任務,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-09-09java Servlet 實現(xiàn)動態(tài)驗證碼圖片示例
這篇文章主要介紹了java Servlet 實現(xiàn)動態(tài)驗證碼圖片示例的資料,這里整理了詳細的代碼,有需要的小伙伴可以參考下。2017-02-02SpringBoot 整合Tess4J庫實現(xiàn)圖片文字識別案例詳解
Tess4J是一個基于Tesseract OCR引擎的Java接口,可以用來識別圖像中的文本,說白了,就是封裝了它的API,讓Java可以直接調用,今天給大家分享一個SpringBoot整合Tess4j庫實現(xiàn)圖片文字識別的小案例2023-10-10Java實現(xiàn)文件和base64流的相互轉換功能示例
這篇文章主要介紹了Java實現(xiàn)文件和base64流的相互轉換功能,涉及Java文件讀取及base64 轉換相關操作技巧,需要的朋友可以參考下2018-05-05