淺談JAVA 類加載器
類加載機制
類加載器負責加載所有的類,系統(tǒng)為所有被載入內存中的類生成一個 java.lang.Class 實例。一旦一個類被載入 JVM 中,同個類就不會被再次載入了?,F(xiàn)在的問題是,怎么樣才算“同一個類”?
正如一個對象有一個唯一的標識一樣,一個載入 JVM 中的類也有一個唯一的標識。在 Java 中,一個類用其全限定類名(包括包名和類名)作為標識:但在 JVM 中,一個類用其全限定類名和其類加載器作為唯一標識。例如,如果在 pg 的包中有一個名為 Person 的類,被類加載器 ClassLoader 的實例 k1 負責加載,則該 Person 類對應的 Class 對象在 JVM 中表示為(Person、pg、k1)。這意味著兩個類加載器加載的同名類:(Person、pg、k1)和(Person、pg、k12)是不同的,它們所加載的類也是完全不同、互不兼容的。
當 JVM 啟動時,會形成由三個類加載器組成的初始類加載器層次結構。
- Bootstrap ClassLoader:根類加載器。
- Extension ClassLoader:擴展類加載器。
- System ClassLoader:系統(tǒng)類加載器。
Bootstrap ClassLoader 被稱為引導(也稱為原始或根)類加載器,它負責加載 Java 的核心類。在Sun 的 JVM 中,當執(zhí)行 java.exe 命令時,使用 -Xbootclasspath 或 -D 選項指定 sun.boot.class.path 系統(tǒng)屬性值可以指定加載附加的類。
JVM的類加載機制主要有如下三種。
- 全盤負責。所謂全盤負責,就是當一個類加載器負責加載某個 Class 時,該 Class 所依賴的和引用的其他 Class 也將由該類加載器負責載入,除非顯式使用另外一個類加載器來載入。
- 父類委托。所謂父類委托,則是先讓 parent(父)類加載器試圖加載該 Class,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。
- 緩存機制。緩存機制將會保證所有加載過的 Class 都會被緩存,當程序中需要使用某個 Class 時,類加載器先從緩存區(qū)中搜尋該 Class,只有當緩存區(qū)中不存在該 Class 對象時,系統(tǒng)才會讀取該類對應的二進制數(shù)據(jù),并將其轉換成 Class 對象,存入緩存區(qū)中。這就是為什么修改了 Class 后,必須重新啟動 JVM,程序所做的修改才會生效的原因。
除了可以使用 Java 提供的類加載器之外,開發(fā)者也可以實現(xiàn)自己的類加載器,自定義的類加載器通過繼承 ClassLoader 來實現(xiàn)。JVM 中這4種類加載器的層次結構如下圖所示。

注意:類加載器之間的父子關系并不是類繼承上的父子關系,這里的父子關系是類加載器實例之間的關系
下面程序示范了訪問 JVM 的類加載器。
public class ClassLoaderPropTest {
public static void main(String[] args) throws IOException {
// 獲取系統(tǒng)類加載器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系統(tǒng)類加載器:" + systemLoader);
/*
* 獲取系統(tǒng)類加載器的加載路徑——通常由CLASSPATH環(huán)境變量指定 如果操作系統(tǒng)沒有指定CLASSPATH環(huán)境變量,默認以當前路徑作為
* 系統(tǒng)類加載器的加載路徑
*/
Enumeration<URL> em1 = systemLoader.getResources("");
while (em1.hasMoreElements()) {
System.out.println(em1.nextElement());
}
// 獲取系統(tǒng)類加載器的父類加載器:得到擴展類加載器
ClassLoader extensionLader = systemLoader.getParent();
System.out.println("擴展類加載器:" + extensionLader);
System.out.println("擴展類加載器的加載路徑:" + System.getProperty("java.ext.dirs"));
System.out.println("擴展類加載器的parent: " + extensionLader.getParent());
}
}
運行上面的程序,會看到如下運行結果
系統(tǒng)類加載器:sun.misc.Launcher$AppClassLoader@73d16e93
file:/F:/EclipseProjects/demo/bin/
擴展類加載器:sun.misc.Launcher$ExtClassLoader@15db9742
擴展類加載器的加載路徑:C:\Program Files\Java\jre1.8.0_181\lib\ext;C:\Windows\Sun\Java\lib\ext
擴展類加載器的parent: null
從上面運行結果可以看出,系統(tǒng)類加載器的加載路徑是程序運行的當前路徑,擴展類加載器的加載路徑是null(與 Java8 有區(qū)別),但此處看到擴展類加載器的父加載器是null,并不是根類加載器。這是因為根類加載器并沒有繼承 ClassLoader 抽象類,所以擴展類加載器的 getParent() 方法返回null。但實際上,擴展類加載器的父類加載器是根類加載器,只是根類加載器并不是 Java 實現(xiàn)的。
從運行結果可以看出,系統(tǒng)類加載器是 AppClassLoader 的實例,擴展類加載器 ExtClassLoader 的實例。實際上,這兩個類都是 URLClassLoader 類的實例。
注意:JVM 的根類加載器并不是 Java 實現(xiàn)的,而且由于程序通常無須訪問根類加載器,因此訪問擴展類加載器的父類加載器時返回null。
類加載器加載 Class 大致要經過如下8個步驟。
- 檢測此 Class 是否載入過(即在緩存區(qū)中是否有此Class),如果有則直接進入第8步,否則接著執(zhí)行第2步。
- 如果父類加載器不存在(如果沒有父類加載器,則要么 parent 一定是根類加載器,要么本身就是根類加載器),則跳到第4步執(zhí)行;如果父類加載器存在,則接著執(zhí)行第3步。
- 請求使用父類加載器去載入目標類,如果成功載入則跳到第8步,否則接著執(zhí)行第5步。
- 請求使用根類加載器來載入目標類,如果成功載入則跳到第8步,否則跳到第7步。
- 當前類加載器嘗試尋找 Class 文件(從與此 ClassLoader 相關的類路徑中尋找),如果找到則執(zhí)行第6步,如果找不到則跳到第7步。
- 從文件中載入 Class,成功載入后跳到第8步。
- 拋出 ClassNotFoundExcepuon 異常。
- 返回對應的 java.lang.Class 對象。
其中,第5、6步允許重寫 ClassLoader的 findClass() 方法來實現(xiàn)自己的載入策略,甚至重寫 loadClass() 方法來實現(xiàn)自己的載入過程。
創(chuàng)建并使用自定義的類加載器
JVM 中除根類加載器之外的所有類加載器都是 ClassLoader 子類的實例,開發(fā)者可以通過擴展 ClassLoader 的子類,并重寫該 ClassLoader 所包含的方法來實現(xiàn)自定義的類加載器。查閱API文檔中關于 ClassLoader 的方法不難發(fā)現(xiàn),ClassLoader 中包含了大量的 protected 方法——這些方法都可被子類重寫。
ClassLoader 類有如下兩個關鍵方法。
- loadClass(String name, boolean resolve):該方法為 ClassLoader 的入口點,根據(jù)指定名稱來加載類,系統(tǒng)就是調用 ClassLoader 的該方法來獲取指定類對應的 Class 對象。
- findClass(String name):根據(jù)指定名稱來查找類。
如果需要實現(xiàn)自定義的 ClassLoader,則可以通過重寫以上兩個方法來實現(xiàn),通常推薦重寫 findClass() 方法,而不是重寫 loadClass() 方法。loadClass() 方法的執(zhí)行步驟如下。
- 用 findLoadedClass(String) 來檢查是否已經加載類,如果已經加載則直接返回。
- 在父類加載器上調用 loadClass() 方法。如果父類加載器為null,則使用根類加載器來加載。
- 調用 findClass(String) 方法查找類。
從上面步驟中可以看出,重寫 findClass()方法可以避免覆蓋默認類加載器的父類委托、緩沖機制兩種策略:如果重寫 loadClass() 方法,則實現(xiàn)邏輯更為復雜。
在 ClassLoader 里還有一個核心方法:Class defineClass(String name, byte[] b, int off,int len) 該方法負責將指定類的字節(jié)碼文件(即 Class 文件,如 Hello.class)讀入字節(jié)數(shù)組 byte[] b 內,并把它轉換為 Class對象,該字節(jié)碼文件可以來源于文件、網絡等。
defineClass() 方法管理 JVM 的許多復雜的實現(xiàn),它負責將字節(jié)碼分析成運行時數(shù)據(jù)結構,并校驗有效性等。不過不用擔心,程序員無須重寫該方法。實際上該方法是 final 的,即使想重寫也沒有機會。
除此之外,ClassLoader 里還包含如下一些普通方法。
- findSystemClass(String name):從本地文件系統(tǒng)裝入文件。它在本地文件系統(tǒng)中尋找類文件,如果存在,就使用 defineClass() 方法將原始字節(jié)轉換成 Class 對象,以將該文件轉換成類。
- static getSystemClassLoader():這是一個靜態(tài)方法,用于返回系統(tǒng)類加載器。
- getParent():獲取該類加載器的父類加載器。
- resolveClass(Class<?> c):鏈接指定的類。類加載器可以使用此方法來鏈接類c。讀者無須理會關于此方法的太多細節(jié)。
- findLoadedClass(String name):如果此 Java 虛擬機已加載了名為 name 的類,則直接返回該類對應的 Class 實例,否則返回null,該方法是 Java 類加載緩存機制的體現(xiàn)。
下面程序開發(fā)了一個自定義的 ClassLoader,該 ClassLoader 通過重寫 findClass() 方法來實現(xiàn)自定義的類加載機制。這個 ClassLoader 可以在加載類之前先編譯該類的文件,從而實現(xiàn)運行 Java 之前先編譯該程序的目標,這樣即可通過該 ClassLoader 直接運行 Java 源文件。
public class CompileClassLoader extends ClassLoader {
// 讀取一個文件的內容
private byte[] getBytes(String filename) throws IOException {
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fin = new FileInputStream(file)) {
// 一次讀取class文件的全部二進制數(shù)據(jù)
int r = fin.read(raw);
if (r != len)
throw new IOException("無法讀取全部文件:" + r + " != " + len);
return raw;
}
}
// 定義編譯指定Java文件的方法
private boolean compile(String javaFile) throws IOException {
System.out.println("CompileClassLoader:正在編譯 " + javaFile + "...");
// 調用系統(tǒng)的javac命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try {
// 其他線程都等待這個線程完成
p.waitFor();
} catch (InterruptedException ie) {
System.out.println(ie);
}
// 獲取javac線程的退出值
int ret = p.exitValue();
// 返回編譯是否成功
return ret == 0;
}
// 重寫ClassLoader的findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
// 將包路徑中的點(.)替換成斜線(/)。
String fileStub = name.replace(".", "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
// 當指定Java源文件存在,且class文件不存在、或者Java源文件
// 的修改時間比class文件修改時間更晚,重新編譯
if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) {
try {
// 如果編譯失敗,或者該Class文件不存在
if (!compile(javaFilename) || !classFile.exists()) {
throw new ClassNotFoundException("ClassNotFoundExcetpion:" + javaFilename);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
// 如果class文件存在,系統(tǒng)負責將該文件轉換成Class對象
if (classFile.exists()) {
try {
// 將class文件的二進制數(shù)據(jù)讀入數(shù)組
byte[] raw = getBytes(classFilename);
// 調用ClassLoader的defineClass方法將二進制數(shù)據(jù)轉換成Class對象
clazz = defineClass(name, raw, 0, raw.length);
} catch (IOException ie) {
ie.printStackTrace();
}
}
// 如果clazz為null,表明加載失敗,則拋出異常
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定義一個主方法
public static void main(String[] args) throws Exception {
// 如果運行該程序時沒有參數(shù),即沒有目標類
if (args.length < 1) {
System.out.println("缺少目標類,請按如下格式運行Java源文件:");
System.out.println("java CompileClassLoader ClassName");
}
// 第一個參數(shù)是需要運行的類
String progClass = args[0];
// 剩下的參數(shù)將作為運行目標類時的參數(shù),
// 將這些參數(shù)復制到一個新數(shù)組中
String[] progArgs = new String[args.length - 1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
// 加載需要運行的類
Class<?> clazz = ccl.loadClass(progClass);
// 獲取需要運行的類的主方法
Method main = clazz.getMethod("main", (new String[0]).getClass());
Object[] argsArray = { progArgs };
main.invoke(null, argsArray);
}
}
上面程序中的粗體字代碼重寫了 findClass() 方法,通過重寫該方法就可以實現(xiàn)自定義的類加載機制。在本類的 findClass() 方法中先檢查需要加載類的 Class 文件是否存在,如果不存在則先編譯源文件,再調用 ClassLoader 的 defineClass() 方法來加載這個 Class 文件,并生成相應的 Class 對象。
接下來可以隨意提供一個簡單的主類,該主類無須編譯就可以使用上面的 CompileClassLoader 來運行它。
public class Hello {
public static void main(String[] args) {
for (String arg : args) {
System.out.println("運行Hello的參數(shù):" + arg);
}
}
}

本示例程序提供的類加載器功能比較簡單,僅僅提供了在運行之前先編譯 Java 源文件的功能。實際上,使用自定義的類加載器,可以實現(xiàn)如下常見功能。
- 執(zhí)行代碼前自動驗證數(shù)字簽名。
- 根據(jù)用戶提供的密碼解密代碼,從而可以實現(xiàn)代碼混淆器來避免反編譯 *.class 文件。
- 根據(jù)用戶需求來動態(tài)地加載類。
- 根據(jù)應用需求把其他數(shù)據(jù)以字節(jié)碼的形式加載到應用中。
URLClassLoader 類
Java 為 ClassLoader 提供了一個 URLClassLoader 實現(xiàn)類,該類也是系統(tǒng)類加載器和擴展類加載器的父類(此處的父類,就是指類與類之間的繼承關系)。URLClassLoader 功能比較強大,它既可以從本地文件系統(tǒng)獲取二進制文件來加載類,也可以從遠程主機獲取二進制文件來加載類。
在應用程序中可以直接使用 URLClassLoader 加載類,URLClassLoader 類提供了如下兩個構造器。
- URLClassLoader(URL[] urls):使用默認的父類加載器創(chuàng)建一個 ClassLoader 對象,該對象將從 urls 所指定的系列路徑來查詢并加載類。
- URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父類加載器創(chuàng)建一個 ClassLoader 對象,其他功能與前一個構造器相同。
一旦得到了 URLClassLoader 對象之后,就可以調用該對象的 loadClass() 方法來加載指定類。下面程序示范了如何直接從文件系統(tǒng)中加載 MySQL 驅動,并使用該驅動來獲取數(shù)據(jù)庫連接。通過這種方式來獲取數(shù)據(jù)厙連接,可以無須將 MySQL 驅動添加到 CLASSPATH 環(huán)境變量中。
public class URLClassLoaderTest {
private static Connection conn;
// 定義一個獲取數(shù)據(jù)庫連接方法
public static Connection getConn(String url, String user, String pass) throws Exception {
if (conn == null) {
// 創(chuàng)建一個URL數(shù)組
URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") };
// 以默認的ClassLoader作為父ClassLoader,創(chuàng)建URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加載MySQL的JDBC驅動,并創(chuàng)建默認實例
Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance();
// 創(chuàng)建一個設置JDBC連接屬性的Properties對象
Properties props = new Properties();
// 至少需要為該對象傳入user和password兩個屬性
props.setProperty("user", user);
props.setProperty("password", pass);
// 調用Driver對象的connect方法來取得數(shù)據(jù)庫連接
conn = driver.connect(url, props);
}
return conn;
}
public static void main(String[] args) throws Exception {
System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147"));
}
}
上面程序中的前兩行粗體字代碼創(chuàng)建了一個 URLClassLoader 對象,該對象使用默認的父類加載器,該類加載器的類加載路徑是當前路徑下的 mysql-connector-java-5.1.30-bin.jar 文件,將 MySQL 驅動復制到該路徑下,這樣保證該 ClassLoader 可以正常加載到 com.mysql.jdbc.Driver 類。
程序的第三行粗體字代碼使用 ClassLoader 的 loadClass() 加載指定類,并調用 Class 對象的 newInstance() 方法創(chuàng)建了一個該類的默認實例——也就是得到 com.mysql.jdbc.Driver 類的對象,當然該對象的實現(xiàn)類實現(xiàn)了 java.sql.Driver 接口,所以程序將其強制類型轉換為 Driver,程序的最后一行粗體字代碼通過 Driver 而不是 DriverManager 來獲取數(shù)據(jù)庫連接,關于 Driver 接口的用法讀者可以自行查閱API文檔。
正如前面所看到的,創(chuàng)建 URLClassLoader 時傳入了一個 URL 數(shù)組參數(shù),該 ClassLoader 就可以從這系列 URL 指定的資源中加載指定類,這里的 URL 可以以 file: 為前綴,表明從本地文件系統(tǒng)加載;可以以 http: 為前綴,表明從互聯(lián)網通過 HTTP 訪問來加載;也可以以 ftp: 為前綴,表明從互聯(lián)網通過 FTP訪問來加載......功能非常強大。
以上就是淺談JAVA 類加載器的詳細內容,更多關于JAVA 類加載器的資料請關注腳本之家其它相關文章!
相關文章
Spring Cloud 部署時使用 Kubernetes 作為注冊中心和配置中
Spring Cloud Kubernetes提供了使用Kubernete本地服務的Spring Cloud通用接口實現(xiàn),這篇文章主要介紹了Spring Cloud 部署時如何使用 Kubernetes 作為注冊中心和配置中心,需要的朋友可以參考下2024-05-05
Java中基于Nacos實現(xiàn)Sentinel規(guī)則持久化詳解
這篇文章主要介紹了Java中基于Nacos實現(xiàn)Sentinel規(guī)則持久化詳解,Sentinel Dashboard中添加的規(guī)則數(shù)據(jù)存儲在內存,微服務停掉規(guī)則數(shù)據(jù)就消失,在?產環(huán)境下不合適,我們可以將Sentinel規(guī)則數(shù)據(jù)持久化到Nacos配置中?,讓微服務從Nacos獲取規(guī)則數(shù)據(jù),需要的朋友可以參考下2023-09-09
SpringBoot整合Ip2region獲取IP地址和定位的詳細過程
ip2region v2.0 - 是一個離線IP地址定位庫和IP定位數(shù)據(jù)管理框架,10微秒級別的查詢效率,提供了眾多主流編程語言的 xdb 數(shù)據(jù)生成和查詢客戶端實現(xiàn) ,這篇文章主要介紹了SpringBoot整合Ip2region獲取IP地址和定位,需要的朋友可以參考下2023-06-06
Java?Controller實現(xiàn)參數(shù)驗證與統(tǒng)一異常處理流程詳細講解
Controller是Spring接受并處理網頁請求的組件,是整個應用的入口,因此學會Controller的常用注解對理解一個應用是重中之重。SpringBoot的Controller中經常會用到注解@Controller、@RestController、@RequestMapping、@RequestBody等2023-01-01

