您好,登錄后才能下訂單哦!
本篇內容主要講解“什么是類加載器”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“什么是類加載器”吧!
Java程序被編譯器編譯之后成為字節碼文件(.class文件),當程序需要某個類時,虛擬機便會將對應的class文件進行加載,創建出對應的Class對象。而這個將class文件加載到虛擬機內存的過程,便是類加載。
類加載器負責在運行時將Java類動態加載到JVM(Java虛擬機),是JRE(Java運行時環境)的一部分。由于類加載器的存在,JVM無需了解底層文件或文件系統即可運行Java程序。
Java類不會一次全部加載到內存中,而是在應用程序需要時才會加載。此時,類加載器負責將類加載到內存中。
類的生命周期通常包括:加載、鏈接、初始化、使用和卸載。上圖中包含了類加載的三個階段:加載階段、鏈接階段和初始化階段。如果將這三個階段再拆分細化包括:加載、驗證、準備、解析和初始化。
關于這幾個階段的作用,已經有很多文章在寫了,我們就簡單概況一下:
加載:通過一個類的完全限定查找類字節碼文件,轉化為方法區運行時的數據結構,創建一個代表該類的Class對象。
驗證:確保Class文件的字節流中包含信息符合當前虛擬機要求,不會危害虛擬機自身安全。
準備:為類變量(即static修飾的字段變量)分配內存并且設置該類變量的初始值。不包含被final修飾的static變量,因為它在編譯時已經分配了。
解析:將常量池內的符號引用轉換為直接引用的過程。如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那么解析將觸發這個類的加載。
初始化:類加載最后階段,若該類具有超類,則對其進行初始化,執行靜態初始化器和靜態初始化成員變量。
在上述類加載的過程中,虛擬機內部提供了三種類加載器:啟動(Bootstrap)類加載器、擴展(Extension)類加載器、系統(System)類加載器(也稱應用類加載器)。
下面就討論不同類型的內置類加載器是如何工作,以及介紹如何自定義類加載器。
先從一個簡單的例子來看一下如何使用不同的加載器來加載不同的類:
public void printClassLoaders() { 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()); }
執行上述程序,打印如下內容:
Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2 Classloader of Logging:sun.misc.Launcher$ExtClassLoader@2f0e140b Classloader of ArrayList:null
上述三行輸出分別對應三種不同的類加載器:系統(System)類加載器、擴展(Extension)類加載器和啟動(Bootstrap)類加載器(顯示為null)。
系統程序類加載器加載包含示例方法的類,也就是將我們自己的文件加載到類路徑中。擴展類加載器加載Logging類,也就是加載作為標準核心Java類擴展的類。啟動類加載器加載ArrayList類,是所有其他類的父級。
對于ArrayList的類加載器,輸出為null。這是因為啟動類加載器是用本機代碼實現而不是Java,因此它不會顯示為Java類。啟動類加載器在操作在不同的JVM中會有所不同。
上述三種類加載器,外加自定義類加載器,它們直接的關系可用下圖表示:
現在來具體看一下這些類加載器。
Java類由java.lang.ClassLoader的實例加載。但是,類加載器本身就是類。那么,誰來加載java.lang.ClassLoader?對,就是啟動類加載器。
啟動類加載器主要負責加載JDK內部類,通常是rt.jar和$JAVA_HOME/jre/lib目錄中的其他核心庫。此外,Bootstrap類加載器還充當所有其他ClassLoader實例的父類。
該啟動程序類加載器是Java虛擬機的一部分,用本機代碼編寫(比如,C++),不同的平臺的實現可能有所不同。
出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類。
擴展類加載器是啟動類加載器的子類,Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現,父類加載器為啟動類加載器,負責加載標準核心Java類的擴展。
擴展類加載器從JDK擴展目錄(通常是$JAVA_HOME/lib/ext目錄)或java.ext.dirs系統屬性中指定的任何其他目錄進行自動加載。
系統類加載器負責將所有應用程序級類加載到JVM中。它加載在類路徑環境變量,-classpath或-cp命令行選項中找到的文件。它是擴展類加載器的子類。
系統類加載器,也稱應用程序加載器是指 Sun公司實現的sun.misc.Launcher$AppClassLoader,負責加載系統類路徑-classpath或-D java.class.path指定路徑下的類庫,也就是我們經常用到的classpath路徑,開發者可以直接使用系統類加載器,一般情況下該類加載是程序中默認的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器。
類加載器是Java運行時環境的一部分。當JVM請求一個類時,類加載器將嘗試定位該類,并使用完全限定名將類定義裝入運行時。
java.lang.ClassLoader.loadClass()方法負責將類定義加載到運行時,它嘗試通過全限定名來加載類。如果未加載到該類,則它將請求委派給父類加載器。依次向上重復該過程。
最終,如果父類加載器找不到指定類,則子類將調用java.net.URLClassLoader.findClass()方法在文件系統本身中查找類。
如果最后一個子類加載器也無法加載該類,則它將拋出java.lang.NoClassDefFoundError或java.lang.ClassNotFoundException。拋出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)
上述過程,通常我們稱作雙親委派機制。雙親委派機制要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器,請注意雙親委派機制中的父子關系并非通常所說的類繼承關系,而是采用組合關系來復用父類加載器的相關代碼。
除外,類加載器還具有三個重要功能:委派模型、類的唯一性和可見性。
類加載器遵循委派模型,在該模型中,根據請求查找類或資源,ClassLoader實例會將對類或資源的搜索委托給父類加載器。
假設我們有一個將應用程序類加載到JVM中的請求。系統類加載器首先將該類的加載委托給其父擴展類加載器,而父擴展類加載器又將其委托給引導類加載器。
僅當啟動類加載器和擴展類加載器都未能成功加載類時,系統類加載器才會嘗試加載類本身。
作為委托模型的結果,很容易確保類的唯一性,因為始終嘗試向上委托。如果父類加載器無法找到該類,則只有當前實例自己會嘗試進行查找和加載。
此外,子類加載器對其父類加載器加載的類可見。例如,系統類加載器加載的類對擴展和Bootstrap類加載器加載的類具有可見性,反之亦然。
比如,通過系統類加載器加載類A,而通過擴展類加載器加載類B,則對系統類加載器加載的其他類而言,A和B類都是可見的。但對擴展類加載器加載的其他類而言,類B是唯一可見的類。
在大多數情況下,如果文件已經在文件系統中,則內置的類加載器就足夠了。但是,在需要從本地硬盤驅動器或網絡中加載類的情況下,可能需要使用自定義類加載器。下面介紹自定義類加載器的使用。
自定義類加載器不僅對在運行時加載類有幫助,還有一些特殊的場景:
幫助修改現有的字節碼,例如weaving agents;
動態創建適合用戶需求的類。例如在JDBC中,通過動態類加載完成不同驅動程序實現之間的切換。
在為具有相同名稱和程序包的類加載不同的字節碼時,實現類版本控制機制。這可以通過URL類加載器(通過URL加載jar)或自定義類加載器來完成。
舉一個更具體的例子,比如,瀏覽器使用自定義類加載器從網站加載可執行內容。瀏覽器可以使用單獨的類加載器從不同的網頁加載applet。用于運行applet的applet查看器包含一個ClassLoader,該類加載器可訪問遠程服務器上的網站,而無需查看本地文件系統。
然后通過HTTP加載原始字節碼文件,并將其轉換為JVM中的類。即使這些applet具有相同的名稱,但如果由不同的類加載器加載,它們也被視為不同的組件。
現在,我們了解了為什么自定義類加載器是相關的,讓我們實現ClassLoader的子類來擴展和總結JVM如何加載類的。
自定義類加載器通常通過繼承java.lang.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; } }
在上面的示例中,我們定義了一個自定義類加載器,該類加載器擴展了默認類加載器并從指定文件加載字節數組。如果沒有太復雜的需求,可以直接繼承URLClassLoader類,重寫loadClass方法,具體可參考AppClassLoader和ExtClassLoader。
下面來看看java.lang.ClassLoader類中的一些基本方法,以更清楚地了解其工作方式。
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)方法以查找類。
protected final Class defineClass( String name, byte[] b, int off, int len) throws ClassFormatError
此方法負責將字節數組轉換為類的實例。如果數據不包含有效的類,則會拋出ClassFormatError。另外,由于此方法被標記為final,因此我們無法覆蓋此方法。
protected Class<?> findClass( String name) throws ClassNotFoundException
此方法查找以標準名稱作為參數的類。我們需要在遵循委派模型加載類的自定義類加載器實現中重寫此方法。
另外,如果父類加載器找不到請求的類,則loadClass()會調用此方法。如果沒有任何類加載器的父類找到該類,則默認實現會拋出ClassNotFoundException異常。
public final ClassLoader getParent()
此方法返回父類加載器以進行委派。某些實現使用null來表示啟動類加載器。
public URL getResource(String name)
此方法嘗試查找具有給定名稱的資源。它將首先委托給資源的父類加載器,如果父級為null,則搜索虛擬機內置的類加載器的路徑。如果失敗,則該方法將調用findResource(String)來查找資源。
指定為輸入的資源名稱可以相對于類路徑,也可以是相對于絕對路徑。
它返回用于讀取資源的URL對象;如果找不到資源或調用者沒有足夠的特權來返回資源,則返回null。
需要注意的是,Java是從類路徑中加載資源。
最后,Java中的資源加載被認為是與位置無關的,因為只要設置了環境來查找資源,代碼在何處運行都無關緊要。
通常,上下文類加載器為J2SE中引入的類加載委托方案提供了一種替代方法。JVM中的類加載器遵循分層模型,因此每個類加載器都有一個單獨的父類,而啟動類加載器除外。但是,有時當JVM核心類需要動態加載應用程序開發人員提供的類或資源時,可能會遇到問題。
例如,在JNDI中,核心功能由rt.jar中的引導程序類實現。但是這些JNDI類可能會加載由獨立供應商實現的JNDI提供程序(部署在應用程序類路徑中)。這種情況要求啟動類加載器(父類加載器)加載對應程序加載器(子類加載器)可見的類。
線程上下文類加載器(context class loader)是從JDK 1.2開始引入的。Java.lang.Thread中的方法 getContextClassLoader()和setContextClassLoader(ClassLoader cl)用來獲取和設置線程的上下文類加載器。如果沒有通過setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java應用運行的初始線程的上下文類加載器是系統類加載器,在線程中運行的代碼可以通過此類加載器來加載類和資源。
線程上下文類加載器從根本解決了一般應用不能違背雙親委派模式的問題,使得java類加載體系顯得更靈活。上面所提到的問題正是線程上下文類加載器的拿手好菜。如果不做任何的設置,Java應用的線程上下文類加載器默認就是系統類加載器。因此,在SPI接口的代碼中使用線程上下文類加載器,就可以成功的加載到SPI實現的類。
到此,相信大家對“什么是類加載器”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。