Java 類(lèi)加載機(jī)制詳細(xì)介紹
一、類(lèi)加載器
類(lèi)加載器(ClassLoader),顧名思義,即加載類(lèi)的東西。在我們使用一個(gè)類(lèi)之前,JVM需要先將該類(lèi)的字節(jié)碼文件(.class文件)從磁盤(pán)、網(wǎng)絡(luò)或其他來(lái)源加載到內(nèi)存中,并對(duì)字節(jié)碼進(jìn)行解析生成對(duì)應(yīng)的Class對(duì)象,這就是類(lèi)加載器的功能。我們可以利用類(lèi)加載器,實(shí)現(xiàn)類(lèi)的動(dòng)態(tài)加載。
二、類(lèi)的加載機(jī)制
在Java中,采用雙親委派機(jī)制來(lái)實(shí)現(xiàn)類(lèi)的加載。那什么是雙親委派機(jī)制?在Java Doc中有這樣一段描述:
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.
從以上描述中,我們可以總結(jié)出如下四點(diǎn):
1、類(lèi)的加載過(guò)程采用委托模式實(shí)現(xiàn)
2、每個(gè) ClassLoader 都有一個(gè)父加載器。
3、類(lèi)加載器在加載類(lèi)之前會(huì)先遞歸的去嘗試使用父加載器加載。
4、虛擬機(jī)有一個(gè)內(nèi)建的啟動(dòng)類(lèi)加載器(bootstrap ClassLoader),該加載器沒(méi)有父加載器,但是可以作為其他加載器的父加載器。
Java 提供三種類(lèi)型的系統(tǒng)類(lèi)加載器。第一種是啟動(dòng)類(lèi)加載器,由C++語(yǔ)言實(shí)現(xiàn),屬于JVM的一部分,其作用是加載 <Java_Runtime_Home>/lib 目錄中的文件,并且該類(lèi)加載器只加載特定名稱(chēng)的文件(如 rt.jar),而不是該目錄下所有的文件。另外兩種是 Java 語(yǔ)言自身實(shí)現(xiàn)的類(lèi)加載器,包括擴(kuò)展類(lèi)加載器(ExtClassLoader)和應(yīng)用類(lèi)加載器(AppClassLoader),擴(kuò)展類(lèi)加載器負(fù)責(zé)加載<Java_Runtime_Home>\lib\ext目錄中或系統(tǒng)變量 java.ext.dirs 所指定的目錄中的文件。應(yīng)用程序類(lèi)加載器負(fù)責(zé)加載用戶(hù)類(lèi)路徑中的文件。用戶(hù)可以直接使用擴(kuò)展類(lèi)加載器或系統(tǒng)類(lèi)加載器來(lái)加載自己的類(lèi),但是用戶(hù)無(wú)法直接使用啟動(dòng)類(lèi)加載器,除了這兩種類(lèi)加載器以外,用戶(hù)也可以自定義類(lèi)加載器,加載流程如下圖所示:

注意:這里父類(lèi)加載器并不是通過(guò)繼承關(guān)系來(lái)實(shí)現(xiàn)的,而是采用組合實(shí)現(xiàn)的。
我們可以通過(guò)一段程序來(lái)驗(yàn)證這個(gè)過(guò)程:
public class Test {
}
public class TestMain {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader!=null){
System.out.println(loader);
loader = loader.getParent();
}
}
}
上面程序的運(yùn)行結(jié)果如下所示:

從結(jié)果我們可以看出,默認(rèn)情況下,用戶(hù)自定義的類(lèi)使用 AppClassLoader 加載,AppClassLoader 的父加載器為 ExtClassLoader,但是 ExtClassLoader 的父加載器卻顯示為空,這是什么原因呢?究其緣由,啟動(dòng)類(lèi)加載器屬于 JVM 的一部分,它不是由 Java 語(yǔ)言實(shí)現(xiàn)的,在 Java 中無(wú)法直接引用,所以才返回空。但如果是這樣,該怎么實(shí)現(xiàn) ExtClassLoader 與 啟動(dòng)類(lèi)加載器之間雙親委派機(jī)制?我們可以參考一下源碼:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
從源碼可以看出,ExtClassLoader 和 AppClassLoader都繼承自 ClassLoader 類(lèi),ClassLoader 類(lèi)中通過(guò) loadClass 方法來(lái)實(shí)現(xiàn)雙親委派機(jī)制。整個(gè)類(lèi)的加載過(guò)程可分為如下三步:
1、查找對(duì)應(yīng)的類(lèi)是否已經(jīng)加載。
2、若未加載,則判斷當(dāng)前類(lèi)加載器的父加載器是否為空,不為空則委托給父類(lèi)去加載,否則調(diào)用啟動(dòng)類(lèi)加載器加載(findBootstrapClassOrNull 再往下會(huì)調(diào)用一個(gè) native 方法)。
3、若第二步加載失敗,則調(diào)用當(dāng)前類(lèi)加載器加載。
通過(guò)上面這段程序,可以很清楚的看出擴(kuò)展類(lèi)加載器與啟動(dòng)類(lèi)加載器之間是如何實(shí)現(xiàn)委托模式的。
現(xiàn)在,我們?cè)衮?yàn)證另一個(gè)問(wèn)題。我們將剛才的Test類(lèi)打成jar包,將其放置在 <Java_Runtime_Home>\lib\ext 目錄下,然后再次運(yùn)行上面的代碼,結(jié)果如下:

現(xiàn)在,該類(lèi)就不再通過(guò) AppClassLoader 來(lái)加載,而是通過(guò) ExtClassLoader 來(lái)加載了。如果我們?cè)噲D把jar包拷貝到<Java_Runtime_Home>\lib,嘗試通過(guò)啟動(dòng)類(lèi)加載器加載該類(lèi)時(shí),我們會(huì)發(fā)現(xiàn)編譯器無(wú)法識(shí)別該類(lèi),因?yàn)閱?dòng)類(lèi)加載器除了指定目錄外,還必須是特定名稱(chēng)的文件才能加載。
三、自定義類(lèi)加載器
通常情況下,我們都是直接使用系統(tǒng)類(lèi)加載器。但是,有的時(shí)候,我們也需要自定義類(lèi)加載器。比如應(yīng)用是通過(guò)網(wǎng)絡(luò)來(lái)傳輸 Java 類(lèi)的字節(jié)碼,為保證安全性,這些字節(jié)碼經(jīng)過(guò)了加密處理,這時(shí)系統(tǒng)類(lèi)加載器就無(wú)法對(duì)其進(jìn)行加載,這樣則需要自定義類(lèi)加載器來(lái)實(shí)現(xiàn)。自定義類(lèi)加載器一般都是繼承自 ClassLoader 類(lèi),從上面對(duì) loadClass 方法來(lái)分析來(lái)看,我們只需要重寫(xiě) findClass 方法即可。下面我們通過(guò)一個(gè)示例來(lái)演示自定義類(lèi)加載器的流程:
package com.paddx.test.classloading;
import java.io.*;
/**
* Created by liuxp on 16/3/12.
*/
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("/Users/liuxp/tmp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.paddx.test.classloading.Test");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
運(yùn)行上面的程序,輸出結(jié)果如下:

自定義類(lèi)加載器的核心在于對(duì)字節(jié)碼文件的獲取,如果是加密的字節(jié)碼則需要在該類(lèi)中對(duì)文件進(jìn)行解密。由于這里只是演示,我并未對(duì)class文件進(jìn)行加密,因此沒(méi)有解密的過(guò)程。這里有幾點(diǎn)需要注意:
1、這里傳遞的文件名需要是類(lèi)的全限定性名稱(chēng),即com.paddx.test.classloading.Test格式的,因?yàn)?defineClass 方法是按這種格式進(jìn)行處理的。
2、最好不要重寫(xiě)loadClass方法,因?yàn)檫@樣容易破壞雙親委托模式。
3、這類(lèi) Test 類(lèi)本身可以被 AppClassLoader 類(lèi)加載,因此我們不能把 com/paddx/test/classloading/Test.class 放在類(lèi)路徑下。否則,由于雙親委托機(jī)制的存在,會(huì)直接導(dǎo)致該類(lèi)由 AppClassLoader 加載,而不會(huì)通過(guò)我們自定義類(lèi)加載器來(lái)加載。
四、總結(jié)
雙親委派機(jī)制能很好地解決類(lèi)加載的統(tǒng)一性問(wèn)題。對(duì)一個(gè) Class 對(duì)象來(lái)說(shuō),如果類(lèi)加載器不同,即便是同一個(gè)字節(jié)碼文件,生成的 Class 對(duì)象也是不等的。也就是說(shuō),類(lèi)加載器相當(dāng)于 Class 對(duì)象的一個(gè)命名空間。雙親委派機(jī)制則保證了基類(lèi)都由相同的類(lèi)加載器加載,這樣就避免了同一個(gè)字節(jié)碼文件被多次加載生成不同的 Class 對(duì)象的問(wèn)題。但雙親委派機(jī)制僅僅是Java 規(guī)范所推薦的一種實(shí)現(xiàn)方式,它并不是強(qiáng)制性的要求。近年來(lái),很多熱部署的技術(shù)都已不遵循這一規(guī)則,如 OSGi 技術(shù)就采用了一種網(wǎng)狀的結(jié)構(gòu),而非雙親委派機(jī)制。
感謝閱讀,希望能幫助到大家,謝謝大家對(duì)本站的支持!
相關(guān)文章
SpringBoot2.x 參數(shù)校驗(yàn)問(wèn)題小結(jié)
這篇文章主要介紹了SpringBoot2.x 參數(shù)校驗(yàn)一些問(wèn)題總結(jié),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08
MyBatis詳細(xì)執(zhí)行流程的全紀(jì)錄
這篇文章主要給大家介紹了關(guān)于MyBatis詳細(xì)執(zhí)行流程的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
Java中EasyPoi導(dǎo)出復(fù)雜合并單元格的方法
這篇文章主要介紹了Java中EasyPoi導(dǎo)出復(fù)雜合并單元格的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
Mybatis實(shí)現(xiàn)傳入多個(gè)參數(shù)的四種方法詳細(xì)講解
這篇文章主要介紹了Mybatis實(shí)現(xiàn)傳入多個(gè)參數(shù)的四種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-01-01
Java JSONObject與JSONArray對(duì)象案例詳解
這篇文章主要介紹了Java JSONObject與JSONArray對(duì)象案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09
解決Servlet4.0版本使用注解設(shè)置url但無(wú)法訪問(wèn)的問(wèn)題
在學(xué)習(xí)servlet過(guò)程中,使用web.xml文件配置servlet可以正常訪問(wèn),但使用WebServlet注解時(shí)出現(xiàn)404錯(cuò)誤,解決方法是在web.xml文件中將metadata-complete屬性改為false,啟動(dòng)標(biāo)注支持,然而該方法對(duì)我無(wú)效,最后通過(guò)重建項(xiàng)目和手動(dòng)將新建的項(xiàng)目添加到tomcat服務(wù)器解決問(wèn)題2024-10-10

