您好,登錄后才能下訂單哦!
java中的Class裝載系統ClassLoader是怎樣使用,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
ClassLoader
在Java中有著非常重要的作用,它主要工作是在Class裝載
的加載階段,主要作用是從系統外部獲得Class二進制數據流
。
ClassLoader
是java的核心組件,所有的Class都是由ClassLoader
進行加載的,ClassLoader
負責通過各種方式將ClassLoader
在整個裝載階段,只能影響類的加載,而無法通過ClassLoader
改變類的連接和初始化行為。
從代碼層面上看,ClassLoader是一個抽象類,它提供了一些重要的接口,用于自定義Class的加載流程和加載方式。ClassLoader的主要方法如下:
public Class<?> loadClass(String name) throws ClassNotFoundException
:給定一個類名,加載一個類,返回代碼這個類的Class實例,如果找不到類,則返回ClassNotFoundException
異常。
protected final Class<?> defineClass(byte[] b, int off, int len)
:根據給定的字節碼流b定義一個類,off
和len
參數表示實際Class信息在byte數組中的位置和長度,其中byte數組b是ClassLoader
從外部獲取的。這是一個受保護的方法,只有在自定義ClassLoader
子類中可以使用。
protected Class<?> findClass(String name) throws ClassNotFoundException
:查找一個類,這是一個受保護的方法,也是重載ClassLoader
時重要的系統擴展點。這個方法在loadClass()
中被調用,用于自定義查找類的邏輯。如果不需要修改類加載默認機制,只是想改變類加載的形式,就可以重載該方法。
protected final Class<?> findLoadedClass(String name)
:這也是一個受保護的方法,它會尋找已經加載的類。這個方法是final方法,無法被修改。
在ClassLoader
的結構中,還有一個重要的字段:parnet
。它也是一個ClassLoader
的實例,這個字段所表示的ClassLoader
稱為這個ClassLoader
的雙親。在類加載的過程中,ClassLoader
可能會將某些請求交給自己的雙親處理。
ClassLoader
的分類在標準的java程序中,java虛擬機會創建3類ClassLoader
為整個應用程序服務。它們分別是:Bootstrap ClassLoader
(啟動類加載器)、Extension ClassLoader
(擴展類加載器)和 App ClassLoader
(應用類加載器,也稱系統類加載器)。此外每一個應用程序還可以擁有自定義的 ClassLoader
,以擴展java虛擬機獲取Class數據的能力。
ClassLoader層次結構如下圖所示。當系統需要使用一個類時,在判斷類是否已經被加載時,會從底層類加載器開始進行判斷。當系統需要加載一個類時,會從頂層類開始加載,依次向下嘗試,直到成功。
啟動類加載器:完全由c語言實現,并且java中沒有對象與之對應,負責加載系統的核心類,比如rt.jar
中的java類。
擴展類加載器:用于加載%JAVA_HOME%/lib/ext/*.jar
中的java類。
應用類加載器:用于加載用戶類,也就是用戶程序的類。
自定義類加載器:用于加載一些特殊途徑的類,一般也是用戶程序的類。
下列代碼輸出了加載的類加載器:
public class Demo04 { public static void main(String[] args) { ClassLoader cl = Demo04.class.getClassLoader(); while (cl != null) { System.out.println(cl.getClass().getName()); cl = cl.getParent(); } } }
代碼中先取得裝載當前類Demo04
的ClassLoader
,然后打印當前ClassLoader
并獲得其雙親,直到類加載器樹被遍歷完成。運行結果如下:
sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader
由此得知,Demo03
是由AppClasLoader
(應用類加載器)加載的,而AppClassLoader
的雙親為ExtClassLoader
(擴展類加載器)。從ExtClassLoader
無法再取得啟動類加載器,因為這是一個系統級的純C語言實現。因此,任何啟動類加載器中加載的類是無法獲得其ClassLoader
實例的,比如:
String.class.getClassLoader()
由于String
屬于java核心類,會被啟動類加載器加載,故以上代碼返回的是null
.
系統中的ClassLoader
在協同工作時,默認會使用雙親委托模式。在類加載的時候,系統會判斷當前類是否已經被加載,如果已經被加載,就會直接返回可用的類,否則就會嘗試加載。在嘗試加載時,會請求雙親處理,如果請求失敗,則會自己加載。
以下代碼顯示了ClassLoader
加載類的詳細過程,它在ClassLoader.loadClass()
中實現:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 檢查類是否已經加載 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) { // 如果雙親不為null // 在雙親加載不成功時,拋出ClassNotFoundException } if (c == null) { // 如果雙親加載不成功 // 使用findClass查找類 long t1 = System.nanoTime(); c = findClass(name); // 定義類加載器,記錄數據 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
判斷類是否加載時,應用類加載器會順著雙親路徑往上判斷,直到啟動類加載器。但是啟動類加載器不會往下詢問,這個委托是單向的。
由前面的分析可知,檢查類是否已加載的委托過程是單向。這種方式雖然從結構上比較清晰,使用各個ClassLoader
的職責非常明確,但是會帶來一個問題:即上層的ClassLoader
無法訪問下層的ClassLoader
所加載的類,如下圖:
通常情況下,啟動類加載器中的類為系統核心類,包括一些重要的系統接口,而在應用類加載器中為應用類。按照這種模式,應用類訪問系統類自然沒問題,但是系統類訪問應用類就會出現問題。比如,在系統類中提供了一個接口,該接口需要在應用中得以實現,還綁定一個工廠方法,用于創建該接口的實例,而接口和工廠方法都在啟動類加載器中。這些就會出現該工廠方法無法創建由應用類加載器的應用實例的問題。
在java平臺中,通常把核心類(rt.jar
)中提供外部服務、可由應用層自行實現的接口稱為Service Provider Interface
,即SPI
.
下面以javax.xml.parsers
中實現XML文件解析功能模塊為例,說明如何在啟動類加載中訪問由應用類加載器實現的SPI
接口實例。
public static DocumentBuilderFactory newInstance() { return FactoryFinder.find( /* The default property name according to the JAXP spec */ DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory" /* The fallback implementation class name */ "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"); }
FactoryFinder.find()
函數試圖加載并返回一個DocumentBuilderFactory
實例。當這個實例在應用層jar包時,它會使用如下方法進行查找:
Object provider = findJarServiceProvider(factoryId);
其中factoryId
就是字條串javax.xml.parsers.DocumentBuilderFactory
,findJarServiceProvider
的主要內容如下代碼所示(這段代碼并非jdK中的源碼,為了展示主要功能,做了刪減):
private static Object findJarServiceProvider(String factoryId) throw ConfigurationError { String serviceId = "META-INF/services" + factoryId; InputStream is = null; ClassLoader cl = ss.getContextClassLoader(); InputStream is = ss.getResourceAsStream(cl, serviceId); BufferedReader rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); String factoryClassName = rd.readLine(); return newInterface(factoryClassName, cl, false, useBSClsLoader); }
從以上代碼可知,系統通過讀取jar包中META-INF/services
目錄下的類名文件讀取工廠類類名,然后根據類名生成對應的實例,并將此ClassLoader
傳入newInstance()
方法,由這個ClassLoader
完成實例的加載和創建,而不是由這段代碼所在的啟動類加載品加載。從而解決了啟動類加載器無法訪問factoryClassName
指定類的問題。
以上代碼中,加載工廠類方法略有曲折,我們平時寫代碼時,知道了一個類的包名.類名
,要生成該類的對象,通常是這么進行的:
Class.forname("包名.類名"),拿到Class對象。
拿到Class對象后,調用Class.newInstance()方法,生成該對象的實例。
但是,在DocumentBuilderFactory
中,這樣做就行不通了,主要原因在于Class.forName()
無法拿到類加載器。我們來看看Class.forName()
的源碼:
public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
從上面可以看到,在哪個類里調用了Class.forName()
,就使用加載那個類的類加載器進行類加載,即DocumentBuilderFactory調用了Class.forName()
,就使用加載DocumentBuilderFactory的類加載器進行加載包名.類名
,但問題是DocumentBuilderFactory是由BootClassLoader加載的,獲取到的類加載器是null,這是無法加載包名.類名
。
雙親模式的類加載方式是虛擬機默認的行為,但并非必須這么做,通過重載ClassLoader
可以修改該行為。下面將演示如何打破默認的雙親模式:
package jvm.chapter10; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; class MyClassLoader extends ClassLoader { private String fileName; public MyClassLoader(String fileName) { this.fileName = fileName; } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class re = findClass(name); if (re != null) { return re; } System.out.println("load class " + name + " failed, parent load start"); return super.loadClass(name, resolve); } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { Class clazz = this.findLoadedClass(className); if (null == clazz) { try { String classFile = getClassFile(className); FileInputStream fis = new FileInputStream(classFile); FileChannel fileChannel = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel outChannel = Channels.newChannel(baos); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while (true) { int i = fileChannel.read(buffer); if (i == 0 || i == -1) { break; } buffer.flip(); outChannel.write(buffer); buffer.clear(); } fis.close(); byte[] bytes = baos.toByteArray(); clazz = defineClass(className, bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } } return clazz; } private String getClassFile(String packageName) { return fileName + packageName.replaceAll("\\.", File.separator) + ".class"; } } /** * {這里添加描述} * * @author chengyan * @date 2019-11-29 4:12 下午 */ public class Demo05 { public static void main(String[] args) throws Exception { MyClassLoader myClassLoader = new MyClassLoader("/Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/"); Class clz = myClassLoader.loadClass("jvm.chapter10.Demo01"); System.out.println(clz.getClassLoader().getClass().getName()); System.out.println("=======class load tree==========="); ClassLoader cl = clz.getClassLoader(); while(cl != null) { System.out.println(cl.getClass().getName()); cl = cl.getParent(); } } }
以上代碼通過自定義ClassLoader重載loadClass()方法,改變了默認的委托雙親加載的方式,運行結果如下:
java.io.FileNotFoundException: /Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/java/lang/Object.class (No such file or directory) at java.io.FileInputStream.open0(Native Method) at java.io.FileInputStream.open(FileInputStream.java:195) at java.io.FileInputStream.<init>(FileInputStream.java:138) at java.io.FileInputStream.<init>(FileInputStream.java:93) at jvm.chapter10.MyClassLoader.findClass(Demo05.java:36) at jvm.chapter10.MyClassLoader.loadClass(Demo05.java:22) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.lang.ClassLoader.defineClass(ClassLoader.java:642) at jvm.chapter10.MyClassLoader.findClass(Demo05.java:52) at jvm.chapter10.MyClassLoader.loadClass(Demo05.java:22) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at jvm.chapter10.Demo05.main(Demo05.java:76) load class java.lang.Object failed, parent load start jvm.chapter10.MyClassLoader =======class load tree=========== jvm.chapter10.MyClassLoader sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader
可以看到,程序首先試圖由MyClassLoader
加載Object
類,但由于指定的路徑中沒有該類信息,故加載失敗,拋出異常,但隨后就由應用類加載器加載成功。接著嘗試加載Demo01
,Demo01
在指定的路徑中,加載成功。打印加載Demo01
的ClassLoader
,顯示為MyClassLoader
,打印ClassLoader
層次,依次為MyClassLoader
,AppClassLoader
,ExtClassLoader
.
關于java中的Class裝載系統ClassLoader是怎樣使用問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。