目錄: - 類加載器
- java.lang.ClassLoader類 URLClassLoader與SecureClassLoader ClassLoader常見方法源碼分析
- 雙親委托機制 圖解 源碼角度分析
- 常見的問題分析
前言:我們剛剛接觸Java時,在IDE(集成開發(fā)環(huán)境) 或者文本編輯器中所寫的都是.java文件,在編譯后會生成.class文件,又稱字節(jié)碼文件。 javac HelloWorld.java ---> HelloWorld.class 復制代碼 對于.class文件來說,需要被加載到虛擬機中才能使用,這個加載的過程就成為類加載。如果想要知道類加載的方式,就需要知道類加載器和雙親委托機制的概念。也就是我們本篇所要介紹的內(nèi)容。
1. 類加載器Java中的類加載器可以分為兩種:
而系統(tǒng)類加載器又有3個: - Bootstrap ClassLoader:啟動類加載器
- Extensions ClassLoader:擴展類加載器
- App ClassLoader:也稱為SystemAppClass,系統(tǒng)類加載器
1.1 Bootstrap ClassLoaderBootstrap ClassLoader用來加載JVM(Java虛擬機)運行時所需要的系統(tǒng)類,其使用c++實現(xiàn)。 從以下路徑來加載類: - %JAVA_HOME%/jre/lib目錄,如rt.jar、resources.jar、charsets.jar等
- 可以在JVM啟動時,指定-Xbootclasspath參數(shù),來改變Bootstrap ClassLoader的加載目錄。
Java虛擬機的啟動就是通過 Bootstrap ClassLoader創(chuàng)建一個初始類來完成的。 可以通過如下代碼來得出Bootstrap ClassLoader所加載的目錄: public class ClassLoaderTest { public static void main(String[]args) { System.out.println(System.getProperty("sun.boot.class.path")); }}復制代碼
打印結(jié)果為: C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_102\jre\classes復制代碼
可以發(fā)現(xiàn)幾乎都是$JAVA_HOME/jre/lib目錄中的jar包,包括rt.jar、resources.jar和charsets.jar等等。 1.2 Extensions ClassLoaderExtensions ClassLoader(擴展類加載器)具體是由ExtClassLoader類實現(xiàn)的,ExtClassLoader類位于sun.misc.Launcher類中,是其的一個靜態(tài)內(nèi)部類。對于Launcher類,可以先看成是Java虛擬機的一個入口。 ExtClassLoader的部分代碼如下:
Extensions ClassLoader負責將JAVA_HOME/jre/lib/ext或者由系統(tǒng)變量-Djava.ext.dir指定位置中的類庫加載到內(nèi)存中。 通過以下代碼可以得到Extensions ClassLoader加載目錄: System.out.println(System.getProperty("java.ext.dirs"));復制代碼
打印結(jié)果為: C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;C:\Windows\Sun\Java\lib\ext復制代碼
1.3 App ClassLoader也稱為SystemAppClass(系統(tǒng)類加載器),具體是由AppClassLoader類實現(xiàn)的,AppClassLoader類也位于sun.misc.Launcher類中。 部分代碼如下:
- 主要加載Classpath目錄下的的所有jar和Class文件,是程序中的默認類加載器。這里的Classpath是指我們Java工程的bin目錄。
- 也可以加載通過-Djava.class.path選項所指定的目錄下的jar和Class文件。
通過以下代碼可以得到App ClassLoader加載目錄: System.out.println(System.getProperty("java.class.path"));復制代碼
打印結(jié)果為: C:\workspace\Demo\bin復制代碼
這個路徑其實就是當前Java工程目錄bin,里面存放的是編譯生成的class文件。
在Java中,除了上述的3種系統(tǒng)提供的類加載器,還可以自定義一個類加載器。 1.4. 自定義類加載器為了可以從指定的目錄下加載jar包或者class文件,我們可以用繼承java.lang.ClassLoader類的方式來實現(xiàn)一個自己的類加載器。 在自定義類加載器時,我們一般復寫findClass方法,并在findClass方法中調(diào)用defineClass方法。 接下來會先介紹下ClassLoader類相關(guān)的具體內(nèi)容,之后看一個自定義類加載器demo。 2 java.lang.ClassLoader類2.1 ClassLoader、URLClassLoader與SecureClassLoader的關(guān)系從上面關(guān)于ExtClassLoader、AppClassLoader源碼圖中我們可以看到,他們都繼承自URLClassLoader,那這個URLClassLoader是什么,其背后又有什么呢? 先來一張很重要的繼承關(guān)系圖:
- ClassLoader是一個抽象類,位于java.lang包下,其中定義了ClassLoader的主要功能。
- SecureClassLoader繼承了抽象類ClassLoader,但SecureClassLoader并不是ClassLoader的實現(xiàn)類,而是拓展了ClassLoader類加入了權(quán)限方面的功能,加強了ClassLoader的安全性。
- URLClassLoader繼承自SecureClassLoader,用來通過URl路徑從jar文件和文件夾中加載類和資源。
- ExtClassLoader和AppClassLoader都繼承自URLClassLoader,它們都是Launcher 的內(nèi)部類,Launcher 是Java虛擬機的入口應用,ExtClassLoader和AppClassLoader都是在Launcher中進行初始化的。
2.2 普通的類、AppClassLoader與ExtClassLoader之間的關(guān)系關(guān)系: - 加載普通的類(這里指得是我們所編寫的代碼類,下文demo中的Test類)加載器是AppClassLoader,AppClassLoader的父加載器為ExtClassLoader
- 而ExtClassLoader的父加載器是Bottstrap ClassLoader
還有2個結(jié)論: 我們準備一個簡單的demo 自建的一個Test.java文件。 public class Test{}復制代碼
public class Main { public static void main(String[] args) {ClassLoader cl = Test.class.getClassLoader();System.out.println("ClassLoader is:"+cl.toString());}}復制代碼
這樣就可以獲取到Test.class文件的類加載器,然后打印出來。結(jié)果是: sun.misc.Launcher$AppClassLoader@75b83e92復制代碼
也就是說明Test.class文件是由AppClassLoader加載的。 那AppClassLoader是誰加載的呢? 其實AppClassLoader也有一個父加載器,我們可以通過以下代碼獲取 public class Test { public static void main(String[] args) { ClassLoader loader = Test.class.getClassLoader(); while (loader != null) { System.out.println(loader); loader = loader.getParent(); } }}復制代碼
上述代碼結(jié)果如下: sun.misc.Launcher$AppClassLoader@7565783bsun.misc.Launcher$ExtClassLoader@1b586d23復制代碼
- 加載Test的類加載器是AppClassLoader,AppClassLoader的父加載器為ExtClassLoader
- 而ExtClassLoader的父加載器是Bottstrap ClassLoader
至于為何沒有打印出ExtClassLoader的父加載器Bootstrap ClassLoader,這是因為Bootstrap ClassLoader是由C++編寫的,并不是一個Java類,因此我們無法在Java代碼中獲取它的引用。
2.3 java.lang.ClassLoader類常見的方法上一節(jié)我們看到了ClassLoader的getParent方法,getParent獲取到的其實就是其父加載器。這一節(jié)將通過源碼,來介紹ClassLoader中的一些重要方法。 getParent()ClassLoader類---------public final ClassLoader getParent() { if (parent == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(parent, Reflection.getCallerClass()); } return parent;}復制代碼
我們可以看到,其返回值有兩種可能,為空或者是parent變量。 從源碼中還可以發(fā)現(xiàn)其是一個final修飾的方法,我們知道被final修飾的說明這個方法提供的功能已經(jīng)滿足當前要求,是不可以重寫的, 所以其各個子類所調(diào)用的getParent()方法最終都會由ClassLoader來處理。
parent變量又是什么呢?我們在查看源碼時可以發(fā)現(xiàn)parent的賦值是在構(gòu)造方法中。 ClassLoader類---------private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; ... //省略了無關(guān)代碼}復制代碼
而此構(gòu)造方法又是私有的,不能被外部調(diào)用,所以其調(diào)用者還是在內(nèi)部。于是接著查找到了另外兩個構(gòu)造方法。 ClassLoader類---------protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader());} protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent);}復制代碼
所以: - 可以在調(diào)用ClassLoder的構(gòu)造方法時,指定一個parent。
- 若沒有指定的話,會使用getSystemClassLoader()方法的返回值。
接著看上面代碼中的getSystemClassLoader的源碼: ClassLoader類---------public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl;}復制代碼
其返回的是一個scl。在initSystemClassLoader()方法中發(fā)現(xiàn)了對scl變量的賦值。 ClassLoader類---------private static synchronized void initSystemClassLoader() { if (!sclSet) { if (scl != null) throw new IllegalStateException("recursive invocation"); sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); //1 if (l != null) { Throwable oops = null; scl = l.getClassLoader(); ...//省略代碼 } sclSet = true; }}復制代碼
重點來了,注釋1處其獲取到的是Launcher類的對象,然后調(diào)用了Launcher類的getClassLoader()方法。 Launcher類---------public ClassLoader getClassLoader() { return this.loader;}復制代碼
那這個this.loader是什么呢?在Launcher類中發(fā)現(xiàn),其賦值操作在Launcher的構(gòu)造方法中,其值正是Launcher類中的AppClassLoader: Launcher類---------public Launcher() { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } ...}復制代碼
到這里謎團全部解開了: 在創(chuàng)建ClassLoder時, - 可以指定一個ClassLoder作為其parent,也就是其父加載器。
- 若沒有指定的話,會使用getSystemClassLoader()方法的返回值(也就是Launcher類中的AppClassLoader)作為其parent。
- 通過getParent()方法可以獲取到這個父加載器。
defineClass()能將class二進制內(nèi)容轉(zhuǎn)換成Class對象,如果不符合要求的會拋出異常,例如ClassFormatError、NoClassDefFoundError。 在自定義ClassLoader時,我們通常會先將特定的文件讀取成byte[]對象,再使用此方法,將其轉(zhuǎn)為class對象。 ClassLoader類---------/*** String name:表示預期的二進制文件名稱,不知道的話,可以填null。* byte[] b:此class文件的二進制數(shù)據(jù)* int off:class二進制數(shù)據(jù)開始的位置* int len:class二進制數(shù)據(jù)的總長度*/protected final Class> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ return defineClass(name, b, off, len, null);}protected final Class> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError{ protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class> c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c;}復制代碼
findClass()findClass()方法一般被loadClass()方法調(diào)用去加載指定名稱類。 ClassLoader類---------/*** String name:class文件的名稱*/protected Class> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name);} 復制代碼
通過源碼看到ClassLoader類中并沒有具體的邏輯,而是等待著其子類去實現(xiàn),通過上面的分析我們知道兩個系統(tǒng)類加載器ExtClassLoader和AppClassLoader都繼承自URLClassLoader,那就來看一下URLClassLoader中的具體代碼。 URLClassLoader類---------protected Class> findClass(final String name) throws ClassNotFoundException{ final Class> result; try { result = AccessController.doPrivileged( new PrivilegedExceptionAction>() { public Class> run() throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); Resource res = ucp.getResource(path, false); if (res != null) { try { return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException(name, e); } ... return result;}private Class> defineClass(String name, Resource res) throws IOException { ... URL url = res.getCodeSourceURL(); ... java.nio.ByteBuffer bb = res.getByteBuffer(); if (bb != null) { ... return defineClass(name, bb, cs); } else { byte[] b = res.getBytes(); ... return defineClass(name, b, 0, b.length, cs); }}復制代碼
可以看到其對傳入的name進行處理后,就調(diào)用了defineClass(name, res);在這個方法里主要是通過res資源和url,加載出相應格式的文件,最終還是通過ClassLoader的defineClass方法加載出具體的類。 loadClass()上節(jié)說到findClass()一般是在loadClass()中調(diào)用,那loadClass()是什么呢? 其實loadClass()就是雙親委托機制的具體實現(xiàn),所以在我們先介紹下雙親委托機制后,再來分析loadClass()。 3 雙親委托機制介紹3.1 圖解雙親委托機制先簡單介紹下雙親委托機制: 類加載器查找Class(也就是在loadClass時)所采用的是雙親委托模式,所謂雙親委托模式就是 - 首先判斷該Class是否已經(jīng)加載
- 如果沒有則不是自身去查找而是委托給父加載器進行查找,這樣依次的進行遞歸,直到委托到最頂層的Bootstrap ClassLoader
- 如果Bootstrap ClassLoader找到了該Class,就會直接返回
- 如果沒找到,則繼續(xù)依次向下查找,如果還沒找到則最后會交由自身去查找
(圖片來自http://liuwangshu.cn/application/classloader/1-java-classloader-.html)
- 其中紅色的箭頭代表向上委托的方向,如果當前的類加載器沒有從緩存中找到這個class對象,就會請求父加載器進行操作。直到Bootstrap ClassLoader。
- 而黑色的箭頭代表的是查找方向,若Bootstrap ClassLoader可以從%JAVA_HOME%/jre/lib目錄或者-Xbootclasspath指定目錄查找到,就直接返回該對象,否則就讓ExtClassLoader去查找。
- ExtClassLoader就會從JAVA_HOME/jre/lib/ext或者-Djava.ext.dir指定位置中查找,找不到時就交給AppClassLoader,AppClassLoader就從當前工程的bin目錄下查找
- 若還是找不到的話,就由我們自定義的CustomClassLoader查找,具體查找的結(jié)果,就要看我們怎么實現(xiàn)自定義ClassLoader的findClass方法了。
3.2 源碼分析雙親委托機制接下來我們看看雙親委托機制在源碼中是如何體現(xiàn)的。 先看loadClass的源碼: ClassLoader類---------protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { //首先,根據(jù)name檢查類是否已經(jīng)加載,若已加載,會直接返回 Class> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //若當前類加載器有父加載器,則調(diào)用其父加載器的loadClass() c = parent.loadClass(name, false); } else { //若當前類加載器的parent為空,則調(diào)用findBootstrapClassOrNull() c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { // 1.如果到這里c依然為空的話,表示一直到最頂層的父加載器也沒有找到已加載的c,那就會調(diào)用findClass進行查找 // 2.在findClass的過程中,如果指定目錄下沒有,就會拋出異常ClassNotFoundException // 3.拋出異常后,此層調(diào)用結(jié)束,接著其子加載器繼續(xù)進行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; }}復制代碼
findBootstrapClassOrNull()方法:可以看到其對name進行校驗后,最終調(diào)用了一個native方法findBootstrapClass()。在findBootstrapClass()方法中最終會用Bootstrap Classloader來查找類。 ClassLoader類---------private Class> findBootstrapClassOrNull(String name){ if (!checkName(name)) return null; return findBootstrapClass(name);} private native Class> findBootstrapClass(String name);復制代碼
4 常見的問題4.1 為什么使用雙親委托機制?- 避免重復加載,如果已經(jīng)加載過一次Class,就不需要再次加載,而是先從緩存中直接讀取。
- 安全方面的考慮,如果不使用雙親委托模式,就可以自定義一個String類來替代系統(tǒng)的String類,這樣便會造成安全隱患,采用雙親委托模式會使得系統(tǒng)的String類在Java虛擬機啟動時就被加載,也就無法自定義String類來替代系統(tǒng)的String類。
4.2 由不同的類加載器加載的類會被JVM當成同一個類嗎?不會。 在Java中,我們用包名+類名作為一個類的標識。 但在JVM中,一個類用其包名+類名和一個ClassLoader的實例作為唯一標識,不同類加載器加載的類將被置于不同的命名空間. 通過一個demo來看, - 用兩個自定義類加載器去加載一個自定義的類
- 然后獲取到的Class進行java.lang.Object.equals(…)判斷。
public class Main { public static void main(String[] args) { ClassLoaderTest myClassLoader = new ClassLoaderTest("F:\\"); ClassLoaderTest myClassLoader2 = new ClassLoaderTest("F:\\"); try { Class c = myClassLoader.loadClass("com.example.Hello"); Class c2 = myClassLoader.loadClass("com.example.Hello"); Class c3 = myClassLoader2.loadClass("com.example.Hello"); System.out.println(c.equals(c2)); //true System.out.println(c.equals(c3)); //flase }}復制代碼
輸出結(jié)果: truefalse復制代碼
只有兩個類名一致并且被同一個類加載器加載的類,Java虛擬機才會認為它們是同一個類。 上面demo中用到的自定義ClassLoader: 自定義的類加載器注意點:1.覆寫findClass方法2.讓其可以根據(jù)name從我們指定的path中加載文件,也就是將文件正確轉(zhuǎn)為byte[]格式3.使用defineClass方法將byte[]數(shù)據(jù)轉(zhuǎn)為Class對象-------------public class ClassLoaderTest extends ClassLoader{ private String path; public ClassLoaderTest(String path) { this.path = path; } @Override protected Class> findClass(String name) throws ClassNotFoundException { Class clazz = null; byte[] classData = classToBytes(name); if (classData == null) { throw new ClassNotFoundException(); } else { clazz= defineClass(name, classData, 0, classData.length); } return clazz; } private byte[] classToBytes(String name) { String fileName = getFileName(name); File file = new File(path,fileName); InputStream in=null; ByteArrayOutputStream out=null; try { in = new FileInputStream(file); out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length=0; while ((length = in.read(buffer)) != -1) { out.write(buffer, 0, length); } return out.toByteArray(); } catch (IOException e) { e.printStackTrace(); }finally { try { if(in!=null) { in.close(); } } catch (IOException e) { e.printStackTrace(); } try{ if(out!=null) { out.close(); } }catch (IOException e){ e.printStackTrace(); } } return null; } private String getFileName(String name) { int index = name.lastIndexOf('.'); if(index == -1){ return name+".class"; }else{ return name.substring(index+1)+".class"; } }}復制代碼
結(jié)語到此Java的類加載器以及雙親委托機制都講了個大概,如果文中有錯誤的地方、或者有其他關(guān)于類加載器比較重要的內(nèi)容又沒有介紹到的,歡迎在評論區(qū)里留言,一起交流學習。 作者:某人Valar 鏈接:https://juejin.im/post/5e094d8df265da339772b7f6 來源:掘金 著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
|