類加載器詳解
首先來了解一下字節碼和class文件的區別:
我們知道,新建一個java對象的時候,JVM要將這個對象對應的字節碼加載到內存中,這個字節碼的原始信息存放在classpath(就是我們新建Java工程的bin目錄下)指定的目錄下的.class文件,類加載需要將.class文件導入到硬盤中,經過一些處理之後變成字節碼在加載到內存中。
下面來看一下簡單的例子:
[java] view plain copy
- package com.loadclass.demo;
- import java.util.Date;
- import java.util.List;
- /**
- * 測試類
- * @author Administrator
- */
- public class ClassLoaderTest {
- @SuppressWarnings("rawtypes")
- public static void main(String[] args){
- //輸出ClassLoaderText的類加載器名稱
- System.out.println("ClassLoaderText類的加載器的名稱:"+ClassLoaderTest.class.getClassLoader().getClass().getName());
- System.out.println("System類的加載器的名稱:"+System.class.getClassLoader());
- System.out.println("List類的加載器的名稱:"+List.class.getClassLoader());
- ClassLoader cl = ClassLoaderTest.class.getClassLoader();
- while(cl != null){
- System.out.print(cl.getClass().getName()+"->");
- cl = cl.getParent();
- }
- System.out.println(cl);
- }
- }
輸出結果:
可以看到,ClassLoaderTest類時由AppClassLoader類加載器加載的。下面就來了解一下JVM中的各個類加載器,同時來解釋一下運行的結果。
Java虛擬機中類加載器:
Java虛擬機中可以安裝多個類加載器,系統默認三個主要的類加載器,每個類負責加載特定位置的類:
BootStrap,ExtClassLoader,AppClassLoader
類加載器也是Java類,因為Java類的類加載器本身也是要被類加載器加載的,顯然必須有第一個類加載器不是Java類,這個正是BootStrap,使用C/C++代碼寫的,已經封裝到JVM內核中了,而ExtClassLoader和AppClassLoader是Java類。
看一下類加載器的屬性結構圖:
Java虛擬機中的所有類加載器采用具有父子關系的樹形結構進行組織,在實例化每個類加載器對象的時候,需要為其指定一個父級類加載器對象或者默認采用系統類加載器為其父級類加載
類加載器的委托機制:
當Java虛擬機要加載第一個類的時候,到底派出哪個類加載器去加載呢?
(1). 首先當前線程的類加載器去加載線程中的第一個類(當前線程的類加載器:Thread類中有一個get/setContextClassLoader(ClassLoader cl);方法,可以獲取/指定本線程中的類加載器)
(2). 如果類A中引用了類B,Java虛擬機將使用加載類A的類加載器來加載類B
(3). 還可以直接調用ClassLoader.loadClass(String className)方法來指定某個類加載器去加載某個類
每個類加載器加載類時,又先委托給其上級類加載器當所有祖宗類加載器沒有加載到類,回到發起者類加載器,還加載不了,則會拋出ClassNotFoundException,不是再去找發起者類加載器的兒子,因為沒有getChild()方法。例如:如上圖所示: MyClassLoader->AppClassLoader->Ext->ClassLoader->BootStrap.自定定義的MyClassLoader1首先會先委托給AppClassLoader,AppClassLoader會委托給ExtClassLoader,ExtClassLoader會委托給BootStrap,這時候BootStrap就去加載,如果加載成功,就結束了。如果加載失敗,就交給ExtClassLoader去加載,如果ExtClassLoader加載成功了,就結束了,如果加載失敗就交給AppClassLoader加載,如果加載成功,就結束了,如果加載失敗,就交給自定義的MyClassLoader1類加載器加載,如果加載失敗,就報ClassNotFoundException異常,結束。
對著類加載器的層次結構圖和委托加載原理,解釋先前的運行的結果
因為System類,List,Map等這樣的系統提供jar類都在rt.jar中,所以由BootStrap類加載器加載,因為BootStrap是祖先類,不是Java編寫的,所以打印出class為null
對於ClassLoaderTest類的加載過程,打印結果也是很清楚的。
現在再來做個試驗來驗證上面的結論:
首先將ClassLoaderTest.java打包成.jar文件(這個步驟就不說了吧,很簡單的)
然後將.jar文件拷貝到Java的安裝目錄中的Java/jre7/lib/ext/目錄下
這時候你在運行ClassLoaderTest類,結果如下:
這時候就發現了ClassLoaderTest的類加載器變成了ExtClassLoader,這時候就說明了上面的結論是正確的,因為ExtClassLoader加載jre/ext/*.jar,首先AppClassLoader類加載器發請求給ExtClassLoader,然後ExtClassLoader發請求給BootStrap,但是BootStrap沒有找到ClassLoaderTest類,所以交給ExtClassLoader處理,這時候ExtClassLoader在my_lib.jar中找到了ClassLoaderTest類,所以就把它加載了,然後結束了。
其實采用這種樹形的類加載機制的好處就在於:
能夠很好的統一管理類加載,首先交給上級,如果上級有了,就加載,這樣如果之前已經加載過的類,這時候在來加載它的時候只要拿過來用就可以了,無需二次加載了
下面來看一下怎麽定義我們自己的一個類加載器MyClassLoader:
自己可以定義類加載器,要將自己定義的類加載器掛載到系統類加載器樹上,在ClassLoader的構造方法中可以指定parent,沒有指定的話,就使用默認的parent
這裏看一下默認的parent是使用getSystemClassLoader方法獲取的,這個方法的源碼沒有找到,所以只能通過代碼來測試一下了
[java] view plain copy
- System.out.println("默認的類加載器:"+ClassLoaderTest.class.getClassLoader().getSystemClassLoader());
輸入結果為:
所以默認的都是將自定義的類加載器掛載到系統類加載器的最低端AppClassLoader,這個也是很合理的。
自定義的類加載器必須繼承抽象類ClassLoader然後重寫findClass方法,其實他內部還有一個loadClass方法和defineClass方法,這兩個方法的作用是:
loadClass方法的源代碼:
[java] view plain copy
- public Class<?> loadClass(String name) throws ClassNotFoundException {
- return loadClass(name, false);
- }
再來看一下loadClass(name,false)方法的源代碼:
[java] view plain copy
- protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
- //加上鎖,同步處理,因為可能是多線程在加載類
- synchronized (getClassLoadingLock(name)) {
- //檢查,是否該類已經加載過了,如果加載過了,就不加載了
- Class c = findLoadedClass(name);
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- //如果自定義的類加載器的parent不為null,就調用parent的loadClass進行加載類
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- //如果自定義的類加載器的parent為null,就調用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加載類失敗,就調用自己的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,看一下源代碼:
[java] view plain copy
- private Class findBootstrapClassOrNull(String name)
- {
- if (!checkName(name)) return null;
- return findBootstrapClass(name);
- }
就是檢查一下name是否是否正確,然後調用findBootstrapClass方法,但是findBootstrapClass方法是個native本地方法,看不到源代碼了,但是可以猜測是用Bootstrap類加載器進行加載類的,這個方法我們也不能重寫,因為如果重寫了這個方法的話,就會破壞這種委托機制,我們還要自己寫一個委托機制,很是蛋疼的。
defineClass這個方法很簡單就是將class文件的字節數組編程一個class對象,這個方法肯定不能重寫,內部實現是在C/C++代碼中實現的
findClass這個方法就是根據name來查找到class文件,在loadClass方法中用到,所以我們只能重寫這個方法了,只要在這個方法中找到class文件,再將它用defineClass方法返回一個Class對象即可。
這三個方法的執行流程是:每個類加載器:loadClass->findClass->defineClass
前期的知識了解後現在就來實現了
首先來看一下需要加載的一個類:ClassLoaderAttachment.java:
[java] view plain copy- package com.loadclass.demo;
- import java.util.Date;
- /**
- * 加載類
- * @author Administrator
- */
- public class ClassLoaderAttachment extends Date{
- private static final long serialVersionUID = 8627644427315706176L;
- //打印數據
- @Override
- public String toString(){
- return "Hello ClassLoader!";
- }
- }
這個類中輸出一段話即可:編譯成ClassLoaderAttachment.class
再來看一下自定義的MyClassLoader.java:
[java] view plain copy
- package com.loadclass.demo;
- import java.io.ByteArrayOutputStream;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.InputStream;
- import java.io.OutputStream;
- /**
- * 自定義的類加載器
- * @author Administrator
- */
- public class MyClassLoader extends ClassLoader{
- //需要加載類.class文件的目錄
- private String classDir;
- //無參的構造方法,用於class.newInstance()構造對象使用
- public MyClassLoader(){
- }
- public MyClassLoader(String classDir){
- this.classDir = classDir;
- }
- @SuppressWarnings("deprecation")
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- //class文件的路徑
- String classPathFile = classDir + "/" + name + ".class";
- try {
- //將class文件進行解密
- FileInputStream fis = new FileInputStream(classPathFile);
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- encodeAndDecode(fis,bos);
- byte[] classByte = bos.toByteArray();
- //將字節流變成一個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{
- //配置運行參數
- 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);
- }
- }
- }
這個類中定義了一個加密和解密的算法,很簡單的,就是將字節和oxff異或一下即可,而且這個算法是加密和解密的都可以用,很是神奇呀!
當然我們還要先做一個操作就是,將ClassLoaderAttachment.class加密後的文件存起來,也就是在main方法中執行的,這裏我是在項目中新建一個class_temp文件夾用來皴法加密後的class文件:
同時采用的是參數的形式來進行賦值的,所以在運行的MyClassLoader的時候要進行輸入參數的配置:右擊MyClassLoader->run as -> run configurations
第一個參數是ClassLoaderAttachment.class文件的源路徑,第二個參數是加密後存放的目錄,運行MyClassLoader之後,刷新class_temp文件夾,出現了ClassLoaderAttachment.class,這個是加密後的class文件。
下面來看一下測試類:
[java] view plain copy- package com.loadclass.demo;
- import java.util.Date;
- import java.util.List;
- /**
- * 測試類
- * @author Administrator
- */
- public class ClassLoaderTest {
- @SuppressWarnings("rawtypes")
- public static void main(String[] args){
- //輸出ClassLoaderText的類加載器名稱
- System.out.println("ClassLoaderText類的加載器的名稱:"+ClassLoaderTest.class.getClassLoader().getClass().getName());
- System.out.println("System類的加載器的名稱:"+System.class.getClassLoader());
- System.out.println("List類的加載器的名稱:"+List.class.getClassLoader());
- System.out.println("默認的類加載器:"+ClassLoaderTest.class.getClassLoader().getSystemClassLoader());
- ClassLoader cl = ClassLoaderTest.class.getClassLoader();
- while(cl != null){
- System.out.print(cl.getClass().getName()+"->");
- cl = cl.getParent();
- }
- System.out.println(cl);
- try {
- Class classDate = new MyClassLoader("class_temp").loadClass("ClassLoaderAttachment");
- Date date = (Date) classDate.newInstance();
- //輸出ClassLoaderAttachment類的加載器名稱
- System.out.println("ClassLoader:"+date.getClass().getClassLoader().getClass().getName());
- System.out.println(date);
- } catch (Exception e1) {
- e1.printStackTrace();
- }
- }
- }
運行ClassLoaderTest類,運行結果如下:
ClassLoaderAttachment類的加載器是我們自己定義的類加載器MyClassLoader,同時也輸出了Hello ClassLoader字段
到此不要以為結束了,這裏還有很多的問題呀,看一下問題的結果是沒有問題,但是這裏面還有很多的東西需要去理解的,首先來看一下,按照我們之前說的類加載機制,應該是先交給父級的類加載器,AppClassLoader->ExtClassLoader->BootStrap,ExtClassLoader和BootStrap沒有找到ClassLoaderAttach.class,但是AppClassLoader類加載器應該能找到呀,可以為什麽也沒有找到呢?這時因為loadClass方法在使用系統類加載器的時候需要傳遞全稱(包括包名),我們傳遞ClassLoaderAttachment的話,AppClassLoader也是沒有找到這個ClassLoaderAttachment,所以還是MyClassLoader處理了,不信的話可以試一下:
現在將
[java] view plain copy- Class classDate = new MyClassLoader("class_temp").loadClass("ClassLoaderAttachment");
改成:
[java] view plain copy
- Class classDate = new MyClassLoader("class_temp").loadClass("com.loadclass.demo.ClassLoaderAttachment");
結果運行:
這時候的加載器是AppClassLoader了,所以要註意loadClass方法傳遞的參數
到這裏我們貌似還沒有測試到我們加密後的class文件,我們現在將工程目錄中的ClassLoaderAttachment.class刪除,將class_temp中加密的ClassLoaderAttachment.class拷貝過去,然後再運行:
這時候就會報錯了,不合適的魔數錯誤(class文件的前六個字節是個魔數用來標識class文件的),這時候就對了,因為ClassLoaderAttachment.class使我們加密後的class文件,AppClassLoader是不認識的,所以報這個錯誤了,只有用我們自己定義的類加載器來進行解密才可以正確的訪問的。到這裏總算是說完了,搞了一上午,頭都寫大了,很是麻煩呀!
註意的兩個問題:
1.可能在測試的過程中有這樣的情況就是ClassLoaderTest類並沒有執行,這個是因為在第一個測試的時候,將ClassLoaderTest類打成.jar放到jre目錄中了,所以你後續修改ClassLoaderTest類的話,運行沒有效果,因為它加載的類還是jre中的jar中的ClassLoaderTest類,所以你應該將jre中的jar刪除即可。
2.就是ClassLoaderAttachment只要保存就會編譯成.class文件,所以你在拷貝ClassLoaderAttachment.class文件的時候要註意了。
轉載:http://blog.csdn.net/jiangwei0910410003/article/details/17733153
類加載器詳解