更新時間:2022-05-19 10:35:02 來源:動力節點 瀏覽1538次
類加載器負責在運行時將 Java 類動態加載到 JVM (Java 虛擬機)。它們也是 JRE(Java 運行時環境)的一部分。因此,借助類加載器,JVM 無需了解底層文件或文件系統即可運行 Java 程序。
此外,這些 Java 類不會一次全部加載到內存中,而是在應用程序需要它們時加載。這就是類加載器發揮作用的地方。他們負責將類加載到內存中。
在本教程中,我們將討論不同類型的內置類加載器及其工作方式。然后我們將介紹我們自己的自定義實現。
讓我們從學習如何使用各種類加載器加載不同的類開始:
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());
}
執行時,上述方法打印:
Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null
正如我們所見,這里有三種不同的類加載器:應用程序、擴展程序和引導程序(顯示為?? null)。
應用程序類加載器加載包含示例方法的類。應用程序或系統類加載器在類路徑中加載我們自己的文件。
接下來,擴展類加載器加載Logging類。擴展類加載器加載作為標準核心 Java 類的擴展的類。
最后,引導類加載器加載ArrayList類。引導程序或原始類加載器是所有其他類加載器的父級。
但是,我們可以看到,對于ArrayList,它在輸出中顯示為null 。這是因為引導類加載器是用本機代碼而不是 Java 編寫的,因此它不會顯示為 Java 類。因此,引導類加載器的行為在不同的 JVM 中會有所不同。
現在讓我們更詳細地討論這些類加載器。
(1)引導類加載器
Java 類由java.lang.ClassLoader的實例加載。但是,類加載器本身就是類。所以問題是,誰加載java.lang.ClassLoader本身?
這就是引導程序或原始類加載器發揮作用的地方。
它主要負責加載 JDK 內部類,通常是rt.jar和其他位于$JAVA_HOME/jre/lib目錄下的核心庫。此外,Bootstrap 類加載器充當所有其他ClassLoader實例的父級。
這個引導類加載器是核心 JVM 的一部分,并且是用本機代碼編寫的,如上面的示例中所指出的。不同的平臺可能有這個特定類加載器的不同實現。
(2)擴展類加載器
擴展類加載器是引導類加載器的子類,負責加載標準核心 Java 類的擴展,以便平臺上運行的所有應用程序都可以使用它們。
擴展類加載器從 JDK 擴展目錄加載,通常是$JAVA_HOME/lib/ext目錄,或java.ext.dirs系統屬性中提到的任何其他目錄。
(3)系統類加載器
另一方面,系統或應用程序類加載器負責將所有應用程序級別的類加載到 JVM 中。它加載在類路徑環境變量、-classpath或-cp命令行選項中找到的文件。它也是擴展類加載器的子類。
類加載器是 Java 運行時環境的一部分。當 JVM 請求一個類時,類加載器會嘗試定位該類并使用完全限定的類名將類定義加載到運行時中。
java.lang.ClassLoader.loadClass()方法負責將類定義加載到運行時。它嘗試根據完全限定名稱加載類。
如果該類尚未加載,它將請求委托給父類加載器。這個過程遞歸地發生。
最終,如果父類加載器沒有找到該類,那么子類將調用java.net.URLClassLoader.findClass()方法在文件系統本身中查找類。
讓我們看一個拋出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)
如果我們從調用java.lang.Class.forName()開始的事件序列,我們可以看到它首先嘗試通過父類加載器加載類,然后java.net.URLClassLoader.findClass()到尋找課程本身。
當它仍然找不到類時,它會拋出ClassNotFoundException。
現在讓我們來看看類加載器的三個重要特性。
(1)委托模型
類加載器遵循委托模型,在請求查找類或資源時,ClassLoader實例會將類或資源的搜索委托給父類加載器。
假設我們有一個將應用程序類加載到 JVM 的請求。系統類加載器首先將該類的加載委托給其父擴展類加載器,后者又將其委托給引導類加載器。
只有當引導程序和擴展類加載器加載類不成功時,系統類加載器才會嘗試加載類本身。
(2)獨特的課程
作為委托模型的結果,很容易確保類的唯一性,因為我們總是嘗試向上委托。
如果父類加載器無法找到該類,那么當前實例才會嘗試自己這樣做。
(3)能見度
此外,子類加載器對其父類加載器加載的類是可見的。
例如,系統類加載器加載的類可以看到擴展和引導類加載器加載的類,反之則不行。
為了說明這一點,如果 A 類由應用程序類加載器加載,而 B 類由擴展類加載器加載,那么就應用程序類加載器加載的其他類而言,A 類和 B 類都是可見的。
但是,B 類是擴展類加載器加載的其他類唯一可見的類。
對于文件已經在文件系統中的大多數情況,內置的類加載器就足夠了。
但是,在我們需要從本地硬盤驅動器或網絡加載類的情況下,我們可能需要使用自定義類加載器。
在本節中,我們將介紹自定義類加載器的其他一些用例,并演示如何創建一個。
(1)自定義類加載器用例
自定義類加載器不僅僅有助于在運行時加載類。一些用例可能包括:
幫助修改現有的字節碼,例如編織代理
動態創建適合用戶需要的類,例如在 JDBC 中,不同驅動程序實現之間的切換是通過動態類加載完成的。
實現類版本控制機制,同時為具有相同名稱和包的類加載不同的字節碼。這可以通過 URL 類加載器(通過 URL 加載 jar)或自定義類加載器來完成。
下面是更具體的示例,自定義類加載器可能會派上用場。
例如,瀏覽器使用自定義類加載器從網站加載可執行內容。瀏覽器可以使用不同的類加載器從不同的網頁加載小程序。用于運行小程序的小程序查看器包含一個訪問遠程服務器上的網站而不是查看本地文件系統的類加載器。
然后它通過 HTTP 加載原始字節碼文件,并將它們轉換為 JVM 中的類。即使這些小程序具有相同的名稱,如果由不同的類加載器加載,它們也會被視為不同的組件。
現在我們了解了為什么自定義類加載器是相關的,讓我們實現一個ClassLoader的子類來擴展和總結 JVM 如何加載類的功能。
(2)創建我們的自定義類加載器
出于說明目的,假設我們需要使用自定義類加載器從文件中加載類。
我們需要擴展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;
}
}
在上面的例子中,我們定義了一個自定義的類加載器,它擴展了默認的類加載器,并從指定的文件中加載一個字節數組。
讓我們討論java.lang.ClassLoader類中的一些基本方法,以更清楚地了解它的工作原理。
(1)loadClass( )方法
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
此方法負責加載給定名稱參數的類。name 參數是指完全限定的類名。
Java 虛擬機調用loadClass()方法來解析類引用,將 resolve 設置為true。但是,并不總是需要解析一個類。如果我們只需要確定類是否存在,則將 resolve 參數設置為false。
此方法用作類加載器的入口點。
我們可以嘗試從java.lang.ClassLoader的源碼中了解loadClass()方法的內部工作原理:
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;
}
}
該方法的默認實現按以下順序搜索類:
調用findLoadedClass(String)方法以查看該類是否已加載。
在父類加載器上調用loadClass(String)方法。
調用findClass(String)方法來查找類。
(2)defineClass ()方法
protected final Class<?> defineClass(
String name, byte[] b, int off, int len) throws ClassFormatError
此方法負責將字節數組轉換為類的實例。在我們使用這個類之前,我們需要解決它。
如果數據不包含有效的類,則會引發ClassFormatError。
此外,我們不能覆蓋這個方法,因為它被標記為 final。
(3)findClass( )方法
protected Class<?> findClass(
String name) throws ClassNotFoundException
此方法查找具有完全限定名稱的類作為參數。我們需要在遵循委托模型的自定義類加載器實現中重寫此方法以加載類。
此外,如果父類加載器找不到請求的類, loadClass()會調用此方法。
如果類加載器的父級沒有找到該類,默認實現會拋出ClassNotFoundException 。
(4)getParent( )方法
public final ClassLoader getParent()
此方法返回用于委托的父類加載器。
一些實現,如之前在第 2 節中看到的實現,使用null來表示引導類加載器。
(5)getResource( )方法
public URL getResource(String name)
此方法嘗試查找具有給定名稱的資源。
它將首先委托給資源的父類加載器。如果 parent 為null,則搜索虛擬機內置的類加載器的路徑。
如果失敗,則該方法將調用findResource(String)來查找資源。指定為輸入的資源名稱對于類路徑可以是相對的或絕對的。
它返回一個用于讀取資源的 URL 對象,如果找不到資源或調用者沒有足夠的權限返回資源,則返回 null。
需要注意的是,Java 從類路徑加載資源。
最后,Java 中的資源加載被認為與位置無關,因為只要將環境設置為查找資源,代碼在哪里運行并不重要。
通常,上下文類加載器提供了一種替代 J2SE 中引入的類加載委托方案的方法。
就像我們之前了解到的,JVM 中的類加載器遵循分層模型,這樣每個類加載器都有一個父類,但引導類加載器除外。
但是,有時當JVM核心類需要動態加載應用程序開發者提供的類或資源時,我們可能會遇到問題。
例如,在 JNDI 中,核心功能由rt.jar 中的引導類實現。但是這些 JNDI 類可能會加載由獨立供應商實現的 JNDI 提供程序(部署在應用程序類路徑中)。此場景要求引導類加載器(父類加載器)加載應用程序加載器(子類加載器)可見的類。
J2SE 委托在這里不起作用,為了解決這個問題,我們需要找到替代的類加載方式。這可以使用線程上下文加載器來實現。
java.lang.Thread類有一個方法getContextClassLoader(),它返回特定線程的ContextClassLoader。ContextClassLoader由線程的創建者在加載資源和類時提供。
以上就是關于“Java類加載機制介紹”,大家如果對此比較感興趣,想了解更多相關知識,不妨來關注一下動力節點的Java基礎教程,里面有更豐富的知識等著大家去學習,相信對大家會有所幫助的。
0基礎 0學費 15天面授
有基礎 直達就業
業余時間 高薪轉行
工作1~3年,加薪神器
工作3~5年,晉升架構
提交申請后,顧問老師會電話與您溝通安排學習