JAVA提高第七篇 類加載器解析
今天我們學習類加載器,關(guān)于類加載器其實和JVM有很大關(guān)系,在這里這篇文章只是簡單的介紹下類加載器,后面學習到JVM的時候還會詳細講到類加載器,本文分為下面幾個小節(jié)講解:
一、認識類加載器
1.什么是類加載器?
所謂的類加載器可以從其作用來理解,其功能就是將classpath目錄下.class文件,加載到內(nèi)存中來進行一些處理,處理完的結(jié)果就是一些字節(jié)碼.那是誰把這些class類加載到內(nèi)存中來的呢?就是類加載器。
2.JVM中默認的類加載器有哪些?
java虛擬機中可以安裝多個類加載器,系統(tǒng)默認三個主要的類加載器,每個類加載器負責加載不同位置的類:BootStrap,ExtClassLoader,AppClassLoader
注意的是:
1.類加載器本身也是一個java類,因為類加載器本身也是一個java類,那么這個特殊的java類【類加載器】是有誰加載進來的呢?這顯然要有第一個類加載器,這第一個類加載器不是一個java類,它是BootStrap。
2.BootStrap不是一個java類,不需要類加載器java加載,他是嵌套在java虛擬機內(nèi)核里面的。java 虛擬機內(nèi)核已啟動的時候,他就已經(jīng)在那里面了,他是用c++語言寫的一段二進制代碼。他可以去加載別的類,其中別的類就包含了類加載器【如上面提到的Ext 和 app】。
案例:
下面我們寫個例子來獲取ClassLoaderTest這個類的類加載器的名字,代碼如下:
package study.javaenhance; import java.util.ArrayList; public class ClassLoaderTest { public static void main(String[] args) throws Exception { //獲取類加載器,那么這個獲取的是一個實例對象,我們知道類加載器也有很多種,那么因此也有其對應的類存在,因此可以獲取到對應的字節(jié)碼 System.out.println(ClassLoaderTest.class.getClassLoader()); //獲取類加載的字節(jié)碼,然后獲取到類加載字節(jié)碼的名字 System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getName()); //下面我們看下獲取非我們定義的類,比如System ArrayList 等常用類 System.out.println(System.class.getClassLoader()); System.out.println(ArrayList.class.getClassLoader()); } }
結(jié)果如下:
sun.misc.Launcher$AppClassLoader@1c78e57
sun.misc.Launcher$AppClassLoader
null
null
結(jié)果分析:
ClassLoaderTest的類加載器的名稱是AppClassLoader。也就是這個類是由AppClassLoader這個類加載器加載的。
System/ArrayList的類加載器是null。這說明這個類加載器是由BootStrap加載的。因為我們上面說了BootStrap不是java類,不需要類加載器加載。所以他的類加載器是null。
==================================
我們說了java給我們提供了三種類加載器:BootStrap,ExtClassLoader,AppClassLoader。這三種類加載器是有父子關(guān)系組成了一個樹形結(jié)構(gòu)。BootStrap是根節(jié)點,BootStrap下面掛著ExtClassLoader,ExtClassLoader下面掛著AppClassLoader.
代碼演示如下:
package study.javaenhance; import java.util.ArrayList; public class ClassLoaderTest { public static void main(String[] args) throws Exception { //獲取類加載器,那么這個獲取的是一個實例對象,我們知道類加載器也有很多種,那么因此也有其對應的類存在,因此可以獲取到對應的字節(jié)碼 System.out.println(ClassLoaderTest.class.getClassLoader()); //獲取類加載的字節(jié)碼,然后獲取到類加載字節(jié)碼的名字 System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getName()); //下面我們看下獲取非我們定義的類,比如System ArrayList 等常用類 System.out.println(System.class.getClassLoader()); System.out.println(ArrayList.class.getClassLoader()); //演示java 提供的類加載器關(guān)系 ClassLoader classloader = ClassLoaderTest.class.getClassLoader(); while(classloader != null) { System.out.print(classloader.getClass().getName()+"-->"); classloader = classloader.getParent(); } System.out.println(classloader); } }
輸出結(jié)果為:
sun.misc.Launcher$AppClassLoader-->sun.misc.Launcher$ExtClassLoader-->null
通過這段程序可以看出來,ClassLoaderTest由AppClassLoader加載,AppClassLoader的父類節(jié)點是ExtClassLoader,ExtClassLoader的父節(jié)點是BootStrap。
每一個類加載器都有自己的管轄范圍。 BootStrap根節(jié)點,只負責加載rt.jar里的類,剛剛那個System就是屬于rt.jar包里面的,ExtClassLoader負責加載JRE/lib/ext/*.jar這個目錄文件夾下的文件。而AppClassLoader負責加載ClassPath目錄下的所有jar文件及目錄。
最后一級是我們自定義的加載器,他們的父類都是AppClassLoader。
二、類加載器的雙親委派機制
除了系統(tǒng)自帶了類加載器,我們還可以自定義類加載器。然后把自己的類加載器掛在樹上。作為某個類加載器的孩子。所有自定義類加載器都要繼承ClassLoader。實現(xiàn)里面的一個方法ClassLoader()如下:
通過上面的知識,我們知道java提供了三個類加載器,而且我們也可以自定義類加載器,并且通過上面的類加載圖也看到了之前的關(guān)系,那么對于一個類的.class 到底是誰去加載呢?
當Java虛擬機要加載第一個類的時候,到底派出哪個類加載器去加載呢?
(1). 首先當前線程的類加載器去加載線程中的第一個類(當前線程的類加載器:Thread類中有一個get/setContextClassLoader(ClassLoader cl);方法,可以獲取/指定本線程中的類加載器)
(2). 如果類A中引用了類B,Java虛擬機將使用加載類A的類加載器來加載類B
(3). 還可以直接調(diào)用ClassLoader.loadClass(String className)方法來指定某個類加載器去加載某個類
每個類加載器加載類時,又先委托給其上級類加載器當所有祖宗類加載器沒有加載到類,回到發(fā)起者類加載器,還加載不了,則會拋出ClassNotFoundException,不是再去找發(fā)起者類加載器的兒子,因為沒有g(shù)etChild()方法。例如:如上圖所示: MyClassLoader->AppClassLoader->Ext->ClassLoader->BootStrap.自定定義的MyClassLoader1首先會先委托給AppClassLoader,AppClassLoader會委托給ExtClassLoader,ExtClassLoader會委托給BootStrap,這時候BootStrap就去加載,如果加載成功,就結(jié)束了。如果加載失敗,就交給ExtClassLoader去加載,如果ExtClassLoader加載成功了,就結(jié)束了,如果加載失敗就交給AppClassLoader加載,如果加載成功,就結(jié)束了,如果加載失敗,就交給自定義的MyClassLoader1類加載器加載,如果加載失敗,就報ClassNotFoundException異常,結(jié)束。
這樣的好處在哪里呢?可以集中管理,不會出現(xiàn)多份字節(jié)碼重復的現(xiàn)象。有兩個類要再在System,如果讓底層的類加載器加載,可能會出現(xiàn)兩份字節(jié)碼。而都讓爺爺加載,爺爺加載到已有,當再有請求過來的時候,爺爺說:哎,我加載過啊,直接把那份拿出來給你用啊。就不會出現(xiàn)多份字節(jié)碼重復的現(xiàn)象。
現(xiàn)在有一道面試題:能不能自己寫一套java.lang.System.?
分析:你寫了也白寫,因為類加載器加載,直接到爺爺那里去找,找成功了,分本就不回來理你的那個。
答案:通常不可以,因為委托機制委托給爺爺,爺爺在rt.jar包加載到這個類以后就不會加載你自己寫了那個System類了。但是,我也有辦法加載,我寫一個自己的類加載器,不讓他用委托機制,不委托給上級了,就可以了.
因為System類,List,Map等這樣的系統(tǒng)提供jar類都在rt.jar中,所以由BootStrap類加載器加載,因為BootStrap是祖先類,不是Java編寫的,所以打印出class為null
對于ClassLoaderTest類的加載過程,打印結(jié)果也是很清楚的。
三、自定義類加載器
下面來看一下怎么定義我們自己的一個類加載器MyClassLoader:
自定義的類加載器必須繼承抽象類ClassLoader然后重寫findClass方法,其實他內(nèi)部還有一個loadClass方法和defineClass方法,這兩個方法的作用是:
loadClass方法的源代碼:
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
再來看一下loadClass(name,false)方法的源代碼:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{ //加上鎖,同步處理,因為可能是多線程在加載類 synchronized (getClassLoadingLock(name)) { //檢查,是否該類已經(jīng)加載過了,如果加載過了,就不加載了 Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //如果自定義的類加載器的parent不為null,就調(diào)用parent的loadClass進行加載類 if (parent != null) { c = parent.loadClass(name, false); } else { //如果自定義的類加載器的parent為null,就調(diào)用findBootstrapClass方法查找類,就是Bootstrap類加載器 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. long t1 = System.nanoTime(); //如果parent加載類失敗,就調(diào)用自己的findClass方法進行類加載 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
在loadClass代碼中也可以看到類加載機制的原理,這里還有這個方法findBootstrapClassOrNull,看一下源代碼:
private Class findBootstrapClassOrNull(String name) { if (!checkName(name)) return null; return findBootstrapClass(name); }
就是檢查一下name是否是否正確,然后調(diào)用findBootstrapClass方法,但是findBootstrapClass方法是個native本地方法,看不到源代碼了,但是可以猜測是用Bootstrap類加載器進行加載類的,這個方法我們也不能重寫,因為如果重寫了這個方法的話,就會破壞這種委托機制,我們還要自己寫一個委托機制。
defineClass這個方法很簡單就是將class文件的字節(jié)數(shù)組編程一個class對象,這個方法肯定不能重寫,內(nèi)部實現(xiàn)是在C/C++代碼中實現(xiàn)的findClass這個方法就是根據(jù)name來查找到class文件,在loadClass方法中用到,所以我們只能重寫這個方法了,只要在這個方法中找到class文件,再將它用defineClass方法返回一個Class對象即可。
這三個方法的執(zhí)行流程是:每個類加載器:loadClass->findClass->defineClass
前期的知識了解后現(xiàn)在就來實現(xiàn)了
首先來看一下需要加載的一個類:ClassLoaderAttachment.java:
package study.javaenhance; public class ClassLoaderAttachment { @Override public String toString() { return "Hello ClassLoader!"; } }
這個類中輸出一段話即可:編譯成ClassLoaderAttachment.class
再來看一下自定義的MyClassLoader.java:
package study.javaenhance; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; public class MyClassLoader extends ClassLoader { //需要加載類.class文件的目錄 private String classDir; //無參的構(gòu)造方法,用于class.newInstance()構(gòu)造對象使用 public MyClassLoader(){ } public MyClassLoader(String classDir){ this.classDir = classDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { System.out.println(name); String classPathFile = classDir + "\\" + name.substring(name.lastIndexOf(".")+1) + ".class"; System.out.println(classPathFile); try { System.out.println("my"); //將class文件進行解密 FileInputStream fis = new FileInputStream(classPathFile); ByteArrayOutputStream bos = new ByteArrayOutputStream(); encodeAndDecode(fis,bos); byte[] classByte = bos.toByteArray(); //將字節(jié)流變成一個class return defineClass(classByte,0,classByte.length); } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } //測試,先將ClassLoaderAttachment.class文件加密寫到工程的class_temp目錄下 public static void main(String[] args) throws Exception{ //配置運行參數(shù) String srcPath = args[0];//ClassLoaderAttachment.class原路徑 String desPath = args[1];//ClassLoaderAttachment.class輸出的路徑 String desFileName = srcPath.substring(srcPath.lastIndexOf("\\")+1); String desPathFile = desPath + "/" + desFileName; FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(desPathFile); //將class進行加密 encodeAndDecode(fis,fos); fis.close(); fos.close(); } /** * 加密和解密算法 * @param is * @param os * @throws Exception */ private static void encodeAndDecode(InputStream is,OutputStream os) throws Exception{ int bytes = -1; while((bytes = is.read())!= -1){ bytes = bytes ^ 0xff;//和0xff進行異或處理 os.write(bytes); } } }
這個類中定義了一個加密和解密的算法,很簡單的,就是將字節(jié)和oxff異或一下即可,而且這個算法是加密和解密的都可以用!
當然我們還要先做一個操作就是,將ClassLoaderAttachment.class加密后的文件存起來,也就是在main方法中執(zhí)行的,這里我是在項目中新建一個
同時采用的是參數(shù)的形式來進行賦值的,所以在運行的MyClassLoader的時候要進行輸入?yún)?shù)的配置:右擊MyClassLoader->run as -> run configurations
第一個參數(shù)是ClassLoaderAttachment.class文件的源路徑,第二個參數(shù)是加密后存放的目錄,運行MyClassLoader之后,刷新class_temp文件夾,出現(xiàn)了ClassLoaderAttachment.class,這個是加密后的class文件。
下面來看一下測試類:
package study.javaenhance; import java.util.ArrayList; public class ClassLoaderTest { public static void main(String[] args) throws Exception { //獲取類加載器,那么這個獲取的是一個實例對象,我們知道類加載器也有很多種,那么因此也有其對應的類存在,因此可以獲取到對應的字節(jié)碼 System.out.println(ClassLoaderTest.class.getClassLoader()); //獲取類加載的字節(jié)碼,然后獲取到類加載字節(jié)碼的名字 System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getName()); //下面我們看下獲取非我們定義的類,比如System ArrayList 等常用類 System.out.println(System.class.getClassLoader()); System.out.println(ArrayList.class.getClassLoader()); //演示java 提供的類加載器關(guān)系 ClassLoader classloader = ClassLoaderTest.class.getClassLoader(); while(classloader != null) { System.out.print(classloader.getClass().getName()+"-->"); classloader = classloader.getParent(); } System.out.println(classloader); try { //Class classDate = new MyClassLoader("class_temp").loadClass("ClassLoaderAttachment"); Class classDate = new MyClassLoader("class_temp").loadClass("study.javaenhance.ClassLoaderAttachment"); Object object = classDate.newInstance(); //輸出ClassLoaderAttachment類的加載器名稱 System.out.println("ClassLoader:"+object.getClass().getClassLoader().getClass().getName()); System.out.println(object); } catch (Exception e1) { e1.printStackTrace(); } } }
結(jié)果如下:
sun.misc.Launcher$AppClassLoader@6b97fd
sun.misc.Launcher$AppClassLoader
null
null
sun.misc.Launcher$AppClassLoader-->sun.misc.Launcher$ExtClassLoader-->null
ClassLoader:sun.misc.Launcher$AppClassLoader
Hello ClassLoader!
這個時候我們會發(fā)現(xiàn)調(diào)用的APP 的類加載器然后輸出了結(jié)果,這個是正常的,因為這個時候會采用雙親委派機制。
那么這個時候,我們將自己生成的ClassLoaderAttachemet class文件,覆蓋掉編譯的時候生成的class 文件看下結(jié)果如何,如果正常應該會報錯,因為這個時候走雙親委派機制在對應的classpath 是可以找到這個class 文件,因此APP類加載器會處理,但是因為我們的class 是加密的因此會報錯,運行結(jié)果如:
那么如何讓其走到我們自定義的類加載器呢,只需要將編譯時候生成的目錄下的.class 文件刪掉即可,那么這個是APP加載不到,則會去調(diào)用findclass ,然后就會走到我們定義的類加載器中,運行結(jié)果如下:
參考資料:
張孝祥老師java增強視頻
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
利用Spring Validation實現(xiàn)輸入驗證功能
這篇文章主要給大家介紹了如何利用Spring Validation完美的實現(xiàn)輸入驗證功能,文中有詳細的代碼示例,具有一定的參考價值,感興趣的朋友可以借鑒一下2023-06-06Java 多線程同步 鎖機制與synchronized深入解析
從尺寸上講,同步代碼塊比同步方法小。你可以把同步代碼塊看成是沒上鎖房間里的一塊用帶鎖的屏風隔開的空間2013-09-09try-cache-finally讀取文件錯誤try-with-resources使用方法
這篇文章主要為大家介紹了try-cache-finally讀取文件錯誤try-with-resources使用方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02java操作(DOM、SAX、JDOM、DOM4J)xml方式的四種比較與詳解
java中四種操作(DOM、SAX、JDOM、DOM4J)xml方式的比較與詳解2008-10-10Hibernate基于ThreadLocal管理Session過程解析
這篇文章主要介紹了Hibernate基于ThreadLocal管理Session過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-10-10Java Springboot 后端使用Mockito庫進行單元測試流程分析
使用Mock進行單元測試可以避免啟動整個Spring框架,節(jié)省時間并降低外部依賴影響,Mock允許模擬外部方法和類,專注于測試方法的功能邏輯,本文給大家介紹Java Springboot 后端使用Mockito庫進行單元測試流程分析,感興趣的朋友跟隨小編一起看看吧2024-10-10Spring、SpringMVC和SpringBoot的區(qū)別及說明
這篇文章主要介紹了Spring、SpringMVC和SpringBoot的區(qū)別及說明,具有很好的參考價值,希望對大家有所幫助。2022-10-10Springboot項目中如何讓非Spring管理的類獲得一個注入的Bean
這篇文章主要介紹了Springboot項目中如何讓非Spring管理的類獲得一個注入的Bean問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12