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

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

本示例程序提供的類加載器功能比較簡單,僅僅提供了在運(yùn)行之前先編譯 Java 源文件的功能。實(shí)際上,使用自定義的類加載器,可以實(shí)現(xiàn)如下常見功能。
- 執(zhí)行代碼前自動驗(yàn)證數(shù)字簽名。
- 根據(jù)用戶提供的密碼解密代碼,從而可以實(shí)現(xiàn)代碼混淆器來避免反編譯 *.class 文件。
- 根據(jù)用戶需求來動態(tài)地加載類。
- 根據(jù)應(yīng)用需求把其他數(shù)據(jù)以字節(jié)碼的形式加載到應(yīng)用中。
URLClassLoader 類
Java 為 ClassLoader 提供了一個(gè) URLClassLoader 實(shí)現(xiàn)類,該類也是系統(tǒng)類加載器和擴(kuò)展類加載器的父類(此處的父類,就是指類與類之間的繼承關(guān)系)。URLClassLoader 功能比較強(qiáng)大,它既可以從本地文件系統(tǒng)獲取二進(jìn)制文件來加載類,也可以從遠(yuǎn)程主機(jī)獲取二進(jìn)制文件來加載類。
在應(yīng)用程序中可以直接使用 URLClassLoader 加載類,URLClassLoader 類提供了如下兩個(gè)構(gòu)造器。
- URLClassLoader(URL[] urls):使用默認(rèn)的父類加載器創(chuàng)建一個(gè) ClassLoader 對象,該對象將從 urls 所指定的系列路徑來查詢并加載類。
- URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父類加載器創(chuàng)建一個(gè) ClassLoader 對象,其他功能與前一個(gè)構(gòu)造器相同。
一旦得到了 URLClassLoader 對象之后,就可以調(diào)用該對象的 loadClass() 方法來加載指定類。下面程序示范了如何直接從文件系統(tǒng)中加載 MySQL 驅(qū)動,并使用該驅(qū)動來獲取數(shù)據(jù)庫連接。通過這種方式來獲取數(shù)據(jù)厙連接,可以無須將 MySQL 驅(qū)動添加到 CLASSPATH 環(huán)境變量中。
public class URLClassLoaderTest {
private static Connection conn;
// 定義一個(gè)獲取數(shù)據(jù)庫連接方法
public static Connection getConn(String url, String user, String pass) throws Exception {
if (conn == null) {
// 創(chuàng)建一個(gè)URL數(shù)組
URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") };
// 以默認(rèn)的ClassLoader作為父ClassLoader,創(chuàng)建URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加載MySQL的JDBC驅(qū)動,并創(chuàng)建默認(rèn)實(shí)例
Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance();
// 創(chuàng)建一個(gè)設(shè)置JDBC連接屬性的Properties對象
Properties props = new Properties();
// 至少需要為該對象傳入user和password兩個(gè)屬性
props.setProperty("user", user);
props.setProperty("password", pass);
// 調(diào)用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)建了一個(gè) URLClassLoader 對象,該對象使用默認(rèn)的父類加載器,該類加載器的類加載路徑是當(dāng)前路徑下的 mysql-connector-java-5.1.30-bin.jar 文件,將 MySQL 驅(qū)動復(fù)制到該路徑下,這樣保證該 ClassLoader 可以正常加載到 com.mysql.jdbc.Driver 類。
程序的第三行粗體字代碼使用 ClassLoader 的 loadClass() 加載指定類,并調(diào)用 Class 對象的 newInstance() 方法創(chuàng)建了一個(gè)該類的默認(rèn)實(shí)例——也就是得到 com.mysql.jdbc.Driver 類的對象,當(dāng)然該對象的實(shí)現(xiàn)類實(shí)現(xiàn)了 java.sql.Driver 接口,所以程序?qū)⑵鋸?qiáng)制類型轉(zhuǎn)換為 Driver,程序的最后一行粗體字代碼通過 Driver 而不是 DriverManager 來獲取數(shù)據(jù)庫連接,關(guān)于 Driver 接口的用法讀者可以自行查閱API文檔。
正如前面所看到的,創(chuàng)建 URLClassLoader 時(shí)傳入了一個(gè) URL 數(shù)組參數(shù),該 ClassLoader 就可以從這系列 URL 指定的資源中加載指定類,這里的 URL 可以以 file: 為前綴,表明從本地文件系統(tǒng)加載;可以以 http: 為前綴,表明從互聯(lián)網(wǎng)通過 HTTP 訪問來加載;也可以以 ftp: 為前綴,表明從互聯(lián)網(wǎng)通過 FTP訪問來加載......功能非常強(qiáng)大。
以上就是淺談JAVA 類加載器的詳細(xì)內(nèi)容,更多關(guān)于JAVA 類加載器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
@MapperScan和@ComponentScan一塊使用導(dǎo)致沖突的解決
這篇文章主要介紹了@MapperScan和@ComponentScan一塊使用導(dǎo)致沖突的解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
詳解Spring Boot 項(xiàng)目部署到heroku爬坑
這篇文章主要介紹了詳解Spring Boot 項(xiàng)目部署到heroku爬坑,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
使用java實(shí)現(xiàn)telnet-client工具分享
這篇文章主要介紹了使用java實(shí)現(xiàn)telnet-client工具,需要的朋友可以參考下2014-03-03
spring基于通用Dao的多數(shù)據(jù)源配置詳解
這篇文章主要為大家詳細(xì)介紹了spring基于通用Dao的多數(shù)據(jù)源配置,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下解2018-03-03

