Java高新技術第一篇 類載入器詳解
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
首先來了解一下位元組碼和class檔案的區別:
我們知道,新建一個java物件的時候,JVM要將這個物件對應的位元組碼載入到記憶體中,這個位元組碼的原始資訊存放在classpath(就是我們新建Java工程的bin目錄下)指定的目錄下的.class檔案,類載入需要將.class檔案匯入到硬碟中,經過一些處理之後變成位元組碼在載入到記憶體中。
下面來看一下簡單的例子:
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方法獲取的,這個方法的原始碼沒有找到,所以只能通過程式碼來測試一下了
System.out.println("預設的類載入器:"+ClassLoaderTest.class.getClassLoader().getSystemClassLoader());
輸入結果為:
所以預設的都是將自定義的類載入器掛載到系統類載入器的最低端AppClassLoader,這個也是很合理的。
自定義的類載入器必須繼承抽象類ClassLoader然後重寫findClass方法,其實他內部還有一個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)) { //檢查,是否該類已經載入過了,如果載入過了,就不載入了 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,看一下原始碼:
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:
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:
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檔案。
下面來看一下測試類:
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處理了,不信的話可以試一下:
現在將
Class classDate = new MyClassLoader("class_temp").loadClass("ClassLoaderAttachment");
改成:
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檔案的時候要注意了。