更新時間:2022-05-19 10:35:02 來源:動力節(jié)點 瀏覽2050次
類加載器負(fù)責(zé)在運行時將 Java 類動態(tài)加載到 JVM (Java 虛擬機(jī))。它們也是 JRE(Java 運行時環(huán)境)的一部分。因此,借助類加載器,JVM 無需了解底層文件或文件系統(tǒng)即可運行 Java 程序。
此外,這些 Java 類不會一次全部加載到內(nèi)存中,而是在應(yīng)用程序需要它們時加載。這就是類加載器發(fā)揮作用的地方。他們負(fù)責(zé)將類加載到內(nèi)存中。
在本教程中,我們將討論不同類型的內(nèi)置類加載器及其工作方式。然后我們將介紹我們自己的自定義實現(xiàn)。
讓我們從學(xué)習(xí)如何使用各種類加載器加載不同的類開始:
public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:"
+ PrintClassLoader.class.getClassLoader());
System.out.println("Classloader of Logging:"
+ Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
執(zhí)行時,上述方法打?。?/p>
Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null
正如我們所見,這里有三種不同的類加載器:應(yīng)用程序、擴(kuò)展程序和引導(dǎo)程序(顯示為?? null)。
應(yīng)用程序類加載器加載包含示例方法的類。應(yīng)用程序或系統(tǒng)類加載器在類路徑中加載我們自己的文件。
接下來,擴(kuò)展類加載器加載Logging類。擴(kuò)展類加載器加載作為標(biāo)準(zhǔn)核心 Java 類的擴(kuò)展的類。
最后,引導(dǎo)類加載器加載ArrayList類。引導(dǎo)程序或原始類加載器是所有其他類加載器的父級。
但是,我們可以看到,對于ArrayList,它在輸出中顯示為null 。這是因為引導(dǎo)類加載器是用本機(jī)代碼而不是 Java 編寫的,因此它不會顯示為 Java 類。因此,引導(dǎo)類加載器的行為在不同的 JVM 中會有所不同。
現(xiàn)在讓我們更詳細(xì)地討論這些類加載器。
(1)引導(dǎo)類加載器
Java 類由java.lang.ClassLoader的實例加載。但是,類加載器本身就是類。所以問題是,誰加載java.lang.ClassLoader本身?
這就是引導(dǎo)程序或原始類加載器發(fā)揮作用的地方。
它主要負(fù)責(zé)加載 JDK 內(nèi)部類,通常是rt.jar和其他位于$JAVA_HOME/jre/lib目錄下的核心庫。此外,Bootstrap 類加載器充當(dāng)所有其他ClassLoader實例的父級。
這個引導(dǎo)類加載器是核心 JVM 的一部分,并且是用本機(jī)代碼編寫的,如上面的示例中所指出的。不同的平臺可能有這個特定類加載器的不同實現(xiàn)。
(2)擴(kuò)展類加載器
擴(kuò)展類加載器是引導(dǎo)類加載器的子類,負(fù)責(zé)加載標(biāo)準(zhǔn)核心 Java 類的擴(kuò)展,以便平臺上運行的所有應(yīng)用程序都可以使用它們。
擴(kuò)展類加載器從 JDK 擴(kuò)展目錄加載,通常是$JAVA_HOME/lib/ext目錄,或java.ext.dirs系統(tǒng)屬性中提到的任何其他目錄。
(3)系統(tǒng)類加載器
另一方面,系統(tǒng)或應(yīng)用程序類加載器負(fù)責(zé)將所有應(yīng)用程序級別的類加載到 JVM 中。它加載在類路徑環(huán)境變量、-classpath或-cp命令行選項中找到的文件。它也是擴(kuò)展類加載器的子類。
類加載器是 Java 運行時環(huán)境的一部分。當(dāng) JVM 請求一個類時,類加載器會嘗試定位該類并使用完全限定的類名將類定義加載到運行時中。
java.lang.ClassLoader.loadClass()方法負(fù)責(zé)將類定義加載到運行時。它嘗試根據(jù)完全限定名稱加載類。
如果該類尚未加載,它將請求委托給父類加載器。這個過程遞歸地發(fā)生。
最終,如果父類加載器沒有找到該類,那么子類將調(diào)用java.net.URLClassLoader.findClass()方法在文件系統(tǒng)本身中查找類。
讓我們看一個拋出ClassNotFoundException時的輸出示例:
java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
如果我們從調(diào)用java.lang.Class.forName()開始的事件序列,我們可以看到它首先嘗試通過父類加載器加載類,然后java.net.URLClassLoader.findClass()到尋找課程本身。
當(dāng)它仍然找不到類時,它會拋出ClassNotFoundException。
現(xiàn)在讓我們來看看類加載器的三個重要特性。
(1)委托模型
類加載器遵循委托模型,在請求查找類或資源時,ClassLoader實例會將類或資源的搜索委托給父類加載器。
假設(shè)我們有一個將應(yīng)用程序類加載到 JVM 的請求。系統(tǒng)類加載器首先將該類的加載委托給其父擴(kuò)展類加載器,后者又將其委托給引導(dǎo)類加載器。
只有當(dāng)引導(dǎo)程序和擴(kuò)展類加載器加載類不成功時,系統(tǒng)類加載器才會嘗試加載類本身。
(2)獨特的課程
作為委托模型的結(jié)果,很容易確保類的唯一性,因為我們總是嘗試向上委托。
如果父類加載器無法找到該類,那么當(dāng)前實例才會嘗試自己這樣做。
(3)能見度
此外,子類加載器對其父類加載器加載的類是可見的。
例如,系統(tǒng)類加載器加載的類可以看到擴(kuò)展和引導(dǎo)類加載器加載的類,反之則不行。
為了說明這一點,如果 A 類由應(yīng)用程序類加載器加載,而 B 類由擴(kuò)展類加載器加載,那么就應(yīng)用程序類加載器加載的其他類而言,A 類和 B 類都是可見的。
但是,B 類是擴(kuò)展類加載器加載的其他類唯一可見的類。
對于文件已經(jīng)在文件系統(tǒng)中的大多數(shù)情況,內(nèi)置的類加載器就足夠了。
但是,在我們需要從本地硬盤驅(qū)動器或網(wǎng)絡(luò)加載類的情況下,我們可能需要使用自定義類加載器。
在本節(jié)中,我們將介紹自定義類加載器的其他一些用例,并演示如何創(chuàng)建一個。
(1)自定義類加載器用例
自定義類加載器不僅僅有助于在運行時加載類。一些用例可能包括:
幫助修改現(xiàn)有的字節(jié)碼,例如編織代理
動態(tài)創(chuàng)建適合用戶需要的類,例如在 JDBC 中,不同驅(qū)動程序?qū)崿F(xiàn)之間的切換是通過動態(tài)類加載完成的。
實現(xiàn)類版本控制機(jī)制,同時為具有相同名稱和包的類加載不同的字節(jié)碼。這可以通過 URL 類加載器(通過 URL 加載 jar)或自定義類加載器來完成。
下面是更具體的示例,自定義類加載器可能會派上用場。
例如,瀏覽器使用自定義類加載器從網(wǎng)站加載可執(zhí)行內(nèi)容。瀏覽器可以使用不同的類加載器從不同的網(wǎng)頁加載小程序。用于運行小程序的小程序查看器包含一個訪問遠(yuǎn)程服務(wù)器上的網(wǎng)站而不是查看本地文件系統(tǒng)的類加載器。
然后它通過 HTTP 加載原始字節(jié)碼文件,并將它們轉(zhuǎn)換為 JVM 中的類。即使這些小程序具有相同的名稱,如果由不同的類加載器加載,它們也會被視為不同的組件。
現(xiàn)在我們了解了為什么自定義類加載器是相關(guān)的,讓我們實現(xiàn)一個ClassLoader的子類來擴(kuò)展和總結(jié) JVM 如何加載類的功能。
(2)創(chuàng)建我們的自定義類加載器
出于說明目的,假設(shè)我們需要使用自定義類加載器從文件中加載類。
我們需要擴(kuò)展ClassLoader類并重寫findClass()方法:
public class CustomClassLoader extends ClassLoader {
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
byteStream.write(nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
}
在上面的例子中,我們定義了一個自定義的類加載器,它擴(kuò)展了默認(rèn)的類加載器,并從指定的文件中加載一個字節(jié)數(shù)組。
讓我們討論java.lang.ClassLoader類中的一些基本方法,以更清楚地了解它的工作原理。
(1)loadClass( )方法
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
此方法負(fù)責(zé)加載給定名稱參數(shù)的類。name 參數(shù)是指完全限定的類名。
Java 虛擬機(jī)調(diào)用loadClass()方法來解析類引用,將 resolve 設(shè)置為true。但是,并不總是需要解析一個類。如果我們只需要確定類是否存在,則將 resolve 參數(shù)設(shè)置為false。
此方法用作類加載器的入口點。
我們可以嘗試從java.lang.ClassLoader的源碼中了解loadClass()方法的內(nèi)部工作原理:
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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
該方法的默認(rèn)實現(xiàn)按以下順序搜索類:
調(diào)用findLoadedClass(String)方法以查看該類是否已加載。
在父類加載器上調(diào)用loadClass(String)方法。
調(diào)用findClass(String)方法來查找類。
(2)defineClass ()方法
protected final Class<?> defineClass(
String name, byte[] b, int off, int len) throws ClassFormatError
此方法負(fù)責(zé)將字節(jié)數(shù)組轉(zhuǎn)換為類的實例。在我們使用這個類之前,我們需要解決它。
如果數(shù)據(jù)不包含有效的類,則會引發(fā)ClassFormatError。
此外,我們不能覆蓋這個方法,因為它被標(biāo)記為 final。
(3)findClass( )方法
protected Class<?> findClass(
String name) throws ClassNotFoundException
此方法查找具有完全限定名稱的類作為參數(shù)。我們需要在遵循委托模型的自定義類加載器實現(xiàn)中重寫此方法以加載類。
此外,如果父類加載器找不到請求的類, loadClass()會調(diào)用此方法。
如果類加載器的父級沒有找到該類,默認(rèn)實現(xiàn)會拋出ClassNotFoundException 。
(4)getParent( )方法
public final ClassLoader getParent()
此方法返回用于委托的父類加載器。
一些實現(xiàn),如之前在第 2 節(jié)中看到的實現(xiàn),使用null來表示引導(dǎo)類加載器。
(5)getResource( )方法
public URL getResource(String name)
此方法嘗試查找具有給定名稱的資源。
它將首先委托給資源的父類加載器。如果 parent 為null,則搜索虛擬機(jī)內(nèi)置的類加載器的路徑。
如果失敗,則該方法將調(diào)用findResource(String)來查找資源。指定為輸入的資源名稱對于類路徑可以是相對的或絕對的。
它返回一個用于讀取資源的 URL 對象,如果找不到資源或調(diào)用者沒有足夠的權(quán)限返回資源,則返回 null。
需要注意的是,Java 從類路徑加載資源。
最后,Java 中的資源加載被認(rèn)為與位置無關(guān),因為只要將環(huán)境設(shè)置為查找資源,代碼在哪里運行并不重要。
通常,上下文類加載器提供了一種替代 J2SE 中引入的類加載委托方案的方法。
就像我們之前了解到的,JVM 中的類加載器遵循分層模型,這樣每個類加載器都有一個父類,但引導(dǎo)類加載器除外。
但是,有時當(dāng)JVM核心類需要動態(tài)加載應(yīng)用程序開發(fā)者提供的類或資源時,我們可能會遇到問題。
例如,在 JNDI 中,核心功能由rt.jar 中的引導(dǎo)類實現(xiàn)。但是這些 JNDI 類可能會加載由獨立供應(yīng)商實現(xiàn)的 JNDI 提供程序(部署在應(yīng)用程序類路徑中)。此場景要求引導(dǎo)類加載器(父類加載器)加載應(yīng)用程序加載器(子類加載器)可見的類。
J2SE 委托在這里不起作用,為了解決這個問題,我們需要找到替代的類加載方式。這可以使用線程上下文加載器來實現(xiàn)。
java.lang.Thread類有一個方法getContextClassLoader(),它返回特定線程的ContextClassLoader。ContextClassLoader由線程的創(chuàng)建者在加載資源和類時提供。
以上就是關(guān)于“Java類加載機(jī)制介紹”,大家如果對此比較感興趣,想了解更多相關(guān)知識,不妨來關(guān)注一下動力節(jié)點的Java基礎(chǔ)教程,里面有更豐富的知識等著大家去學(xué)習(xí),相信對大家會有所幫助的。
相關(guān)閱讀