擴(kuò)展類加載器:由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載JAVA_HOME下lib\ext目錄下的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫(kù),開(kāi)發(fā)者可以直接使用擴(kuò)展類加載器。
應(yīng)用類加載器:由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)的。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑上所指定的類庫(kù),可以被直接使用。如果未自定義類加載器,默認(rèn)為該類加載器。
可以通過(guò)這種方式打印加載路徑及相關(guān)jar:
System.out.println("boot:" System.getProperty("sun.boot.class.path"));System.out.println("ext:" System.getProperty("java.ext.dirs"));System.out.println("app:" System.getProperty("java.class.path"));
在打印的日志中,可以看到詳細(xì)的路徑以及路徑下面都包含了哪些類庫(kù)。由于打印內(nèi)容較多,這里就不展示了。
除啟動(dòng)類加載器外,擴(kuò)展類加載器和應(yīng)用類加載器都是通過(guò)類sun.misc.Launcher進(jìn)行初始化,而Launcher類則由根類加載器進(jìn)行加載。相關(guān)代碼如下:
public Launcher() { Launcher.ExtClassLoader var1; try { //初始化擴(kuò)展類加載器,構(gòu)造函數(shù)沒(méi)有入?yún)?,無(wú)法獲取啟動(dòng)類加載器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { //初始化應(yīng)用類加載器,入?yún)閿U(kuò)展類加載器 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } // 設(shè)置上下文類加載器 Thread.currentThread().setContextClassLoader(this.loader); //...}
雙親委派模型:當(dāng)一個(gè)類加載器接收到類加載請(qǐng)求時(shí),會(huì)先請(qǐng)求其父類加載器加載,依次遞歸,當(dāng)父類加載器無(wú)法找到該類時(shí)(根據(jù)類的全限定名稱),子類加載器才會(huì)嘗試去加載。
雙親委派中的父子關(guān)系一般不會(huì)以繼承的方式來(lái)實(shí)現(xiàn),而都是使用組合的關(guān)系來(lái)復(fù)用父加載器的代碼。
通過(guò)編寫測(cè)試代碼,進(jìn)行debug,可以發(fā)現(xiàn)雙親委派過(guò)程中不同類加載器之間的組合關(guān)系。
而這一過(guò)程借用一張時(shí)序圖來(lái)查看會(huì)更加清晰。
ClassLoader類是一個(gè)抽象類,但卻沒(méi)有包含任何抽象方法。繼承ClassLoader類并重寫findClass方法便可實(shí)現(xiàn)自定義類加載器。但如果破壞上面所述的雙親委派模型來(lái)實(shí)現(xiàn)自定義類加載器,則需要繼承ClassLoader類并重寫loadClass方法和findClass方法。
ClassLoader類的部分源碼如下:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{ //進(jìn)行類加載操作時(shí)首先要加鎖,避免并發(fā)加載 synchronized (getClassLoadingLock(name)) { //首先判斷指定類是否已經(jīng)被加載過(guò) Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //如果當(dāng)前類沒(méi)有被加載且父類加載器不為null,則請(qǐng)求父類加載器進(jìn)行加載操作 c = parent.loadClass(name, false); } else { //如果當(dāng)前類沒(méi)有被加載且父類加載器為null,則請(qǐng)求根類加載器進(jìn)行加載操作 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { long t1 = System.nanoTime(); //如果父類加載器加載失敗,則由當(dāng)前類加載器進(jìn)行加載, c = findClass(name); //進(jìn)行一些統(tǒng)計(jì)操作 // ... } } //初始化該類 if (resolve) { resolveClass(c); } return c; }}
上面代碼中也提現(xiàn)了不同類加載器之間的層級(jí)及組合關(guān)系。
雙親委派模型是為了保證Java核心庫(kù)的類型安全。所有Java應(yīng)用都至少需要引用java.lang.Object類,在運(yùn)行時(shí)這個(gè)類需要被加載到Java虛擬機(jī)中。如果該加載過(guò)程由自定義類加載器來(lái)完成,可能就會(huì)存在多個(gè)版本的java.lang.Object類,而且這些類之間是不兼容的。
通過(guò)雙親委派模型,對(duì)于Java核心庫(kù)的類的加載工作由啟動(dòng)類加載器來(lái)統(tǒng)一完成,保證了Java應(yīng)用所使用的都是同一個(gè)版本的Java核心庫(kù)的類,是互相兼容的。
子類加載器都保留了父類加載器的引用。但如果父類加載器加載的類需要訪問(wèn)子類加載器加載的類該如何處理?最經(jīng)典的場(chǎng)景就是JDBC的加載。
JDBC是Java制定的一套訪問(wèn)數(shù)據(jù)庫(kù)的標(biāo)準(zhǔn)接口,它包含在Java基礎(chǔ)類庫(kù)中,由根類加載器加載。而各個(gè)數(shù)據(jù)庫(kù)廠商的實(shí)現(xiàn)類庫(kù)是作為第三方依賴引入使用的,這部分實(shí)現(xiàn)類庫(kù)是由應(yīng)用類加載器進(jìn)行加載的。
獲取Mysql連接的代碼:
//加載驅(qū)動(dòng)程序Class.forName("com.mysql.jdbc.Driver");//連接數(shù)據(jù)庫(kù)Connection conn = DriverManager.getConnection(url, user, password);
DriverManager由啟動(dòng)類加載器加載,它使用到的數(shù)據(jù)庫(kù)驅(qū)動(dòng)(com.mysql.jdbc.Driver)是由應(yīng)用類加載器加載的,這就是典型的由父類加載器加載的類需要訪問(wèn)由子類加載器加載的類。
這一過(guò)程的實(shí)現(xiàn),看DriverManager類的源碼:
//建立數(shù)據(jù)庫(kù)連接底層方法private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { //獲取調(diào)用者的類加載器 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { //由啟動(dòng)類加載器加載的類,該值為null,使用上下文類加載器 if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } //... for(DriverInfo aDriver : registeredDrivers) { //使用上下文類加載器去加載驅(qū)動(dòng) if(isDriverAllowed(aDriver.driver, callerCL)) { try { //加載成功,則進(jìn)行連接 Connection con = aDriver.driver.connect(url, info); //... } catch (SQLException ex) { if (reason == null) { reason = ex; } } } //... }}
在上面的代碼中留意改行代碼:
callerCL = Thread.currentThread().getContextClassLoader();
這行代碼從當(dāng)前線程中獲取ContextClassLoader,而ContextClassLoader在哪里設(shè)置呢?就是在上面的Launcher源碼中設(shè)置的:
// 設(shè)置上下文類加載器Thread.currentThread().setContextClassLoader(this.loader);
這樣一來(lái),所謂的上下文類加載器本質(zhì)上就是應(yīng)用類加載器。因此,上下文類加載器只是為了解決類的逆向訪問(wèn)提出來(lái)的一個(gè)概念,并不是一個(gè)全新的類加載器,本質(zhì)上是應(yīng)用類加載器。
自定義類加載器只需要繼承java.lang.ClassLoader類,然后重寫findClass(String name)方法即可,在方法中指明如何獲取類的字節(jié)碼流。
如果要破壞雙親委派規(guī)范的話,還需重寫loadClass方法(雙親委派的具體邏輯實(shí)現(xiàn))。但不建議這么做。
public class ClassLoaderTest extends ClassLoader { private String classPath; public ClassLoaderTest(String classPath) { this.classPath = classPath; } /** * 編寫findClass方法的邏輯 * * @param name * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 獲取類的class文件字節(jié)數(shù)組 byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { // 生成class對(duì)象 return defineClass(name, classData, 0, classData.length); } } /** * 編寫獲取class文件并轉(zhuǎn)換為字節(jié)碼流的邏輯 * * @param className * @return */ private byte[] getClassData(String className) { // 讀取類文件的字節(jié) String path = classNameToPath(className); try { InputStream is = new FileInputStream(path); ByteArrayOutputStream stream = new ByteArrayOutputStream(); byte[] buffer = new byte[2048]; int num = 0; // 讀取類文件的字節(jié)碼 while ((num = is.read(buffer)) != -1) { stream.write(buffer, 0, num); } return stream.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 類文件的完全路徑 * * @param className * @return */ private String classNameToPath(String className) { return classPath File.separatorChar className.replace('.', File.separatorChar) ".class"; } public static void main(String[] args) { String classPath = "/Users/zzs/my/article/projects/java-stream/src/main/java/"; ClassLoaderTest loader = new ClassLoaderTest(classPath); try { //加載指定的class文件 Class<?> object1 = loader.loadClass("com.secbro2.classload.SubClass"); System.out.println(object1.newInstance().toString()); } catch (Exception e) { e.printStackTrace(); } }}
打印結(jié)果:
SuperClass static initSubClass static initcom.secbro2.classload.SubClass@5451c3a8
關(guān)于SuperClass和SubClass在上篇文章《面試官,不要再問(wèn)我“Java虛擬機(jī)類加載機(jī)制”了》已經(jīng)貼過(guò)代碼,這里就不再貼出了。
通過(guò)上面的代碼可以看出,主要重寫了findClass獲取class的路徑便實(shí)現(xiàn)了自定義的類加載器。
那么,什么場(chǎng)景會(huì)用到自定義類加載器呢?當(dāng)JDK提供的類加載器實(shí)現(xiàn)無(wú)法滿足我們的需求時(shí),才需要自己實(shí)現(xiàn)類加載器。比如,OSGi、代碼熱部署等領(lǐng)域。
以上類加載器模型為Java8以前版本,在Java9中類加載器已經(jīng)發(fā)生了變化。在這里主要簡(jiǎn)單介紹一下相關(guān)模型的變化,具體變化細(xì)節(jié)就不再這里展開(kāi)了。
java9中目錄的改變。
Java9中類加載器的改變。
在java9中,應(yīng)用程序類加載器可以委托給平臺(tái)類加載器以及啟動(dòng)類加載器;平臺(tái)類加載器可以委托給啟動(dòng)類加載器和應(yīng)用程序類加載器。
在java9中,啟動(dòng)類加載器是由類庫(kù)和代碼在虛擬機(jī)中實(shí)現(xiàn)的。為了向后兼容,在程序中仍然由null表示。例如,Object.class.getClassLoader()仍然返回null。但是,并不是所有的JavaSE平臺(tái)和JDK模塊都由啟動(dòng)類加載器加載。
舉幾個(gè)例子,啟動(dòng)類加載器加載的模塊是java.base,java.logging,java.prefs和java.desktop。其他JavaSE平臺(tái)和JDK模塊由平臺(tái)類加載器和應(yīng)用程序類加載器加載。
java9中不再支持用于指定引導(dǎo)類路徑,-Xbootclasspath和-Xbootclasspath/p選項(xiàng)以及系統(tǒng)屬性sun.boot.class.path。-Xbootclasspath/a選項(xiàng)仍然受支持,其值存儲(chǔ)在jdk.boot.class.path.append的系統(tǒng)屬性中。
java9不再支持?jǐn)U展機(jī)制。但是,它將擴(kuò)展類加載器保留在名為平臺(tái)類加載器的新名稱下。ClassLoader類包含一個(gè)名為getPlatformClassLoader()的靜態(tài)方法,該方法返回對(duì)平臺(tái)類加載器的引用。
本篇文章主要基于java8介紹了Java虛擬機(jī)類加載器及雙親委派機(jī)制,和Java8中的一些變化。其中,java9中更深層次的變化,大家可以進(jìn)一步研究一下。該系列持續(xù)更新中,“程序新視界”。
聯(lián)系客服