城市獵人

          在一網(wǎng)情深的日子里,誰能說得清是苦是甜,只知道確定了就義無反顧
          posts - 1, comments - 7, trackbacks - 0, articles - 89

          類加載器

          Posted on 2008-10-19 08:31 sailor 閱讀(736) 評論(0)  編輯  收藏 所屬分類: java
          來源:網(wǎng)絡(luò)
           

          類加載器

          Java中類的查找與裝載出現(xiàn)的問題總是會時不時出現(xiàn)在Java程序員面前,這并不是什么丟臉的事情,相信沒有一個Java程序員沒遇到過ClassNotException,因此不要為被人瞅見自己也犯這樣的錯誤而覺得不自然,但是在如果出現(xiàn)了ClassNotFoundException后異常后一臉的茫然,那我想你該了解一下java的類裝載的體制了,同時為了進行下面的關(guān)于類裝載器之間的隔離性的討論,我們先簡單介紹一下類裝載的體系結(jié)構(gòu)。

          1. Java類裝載體系結(jié)構(gòu)

          裝載類的過程非常簡單:查找類所在位置,并將找到的Java類的字節(jié)碼裝入內(nèi)存,生成對應(yīng)的Class對象。Java的類裝載器專門用來實現(xiàn)這樣的過程,JVM并不止有一個類裝載器,事實上,如果你愿意的話,你可以讓JVM擁有無數(shù)個類裝載器,當(dāng)然這除了測試JVM外,我想不出還有其他的用途。你應(yīng)該已經(jīng)發(fā)現(xiàn)到了這樣一個問題,類裝載器自身也是一個類,它也需要被裝載到內(nèi)存中來,那么這些類裝載器由誰來裝載呢,總得有個根吧?沒錯,確實存在這樣的根,它就是神龍見首不見尾的Bootstrap ClassLoader. 為什么說它神龍見首不見尾呢,因為你根本無法在Java代碼中抓住哪怕是它的一點點的尾巴,盡管你能時時刻刻體會到它的存在,因為java的運行環(huán)境所需要的所有類庫,都由它來裝載,而它本身是C++寫的程序,可以獨立運行,可以說是JVM的運行起點,偉大吧。在Bootstrap完成它的任務(wù)后,會生成一個AppClassLoader(實際上之前系統(tǒng)還會使用擴展類裝載器ExtClassLoader,它用于裝載Java運行環(huán)境擴展包中的類),這個類裝載器才是我們經(jīng)常使用的,可以調(diào)用ClassLoader.getSystemClassLoader() 來獲得,我們假定程序中沒有使用類裝載器相關(guān)操作設(shè)定或者自定義新的類裝載器,那么我們編寫的所有java類通通會由它來裝載,值得尊敬吧。AppClassLoader查找類的區(qū)域就是耳熟能詳?shù)?/span>Classpath,也是初學(xué)者必須跨過的門檻,有沒有靈光一閃的感覺,我們按照它的類查找范圍給它取名為類路徑類裝載器。還是先前假定的情況,當(dāng)Java中出現(xiàn)新的類,AppClassLoader首先在類傳遞給它的父類類裝載器,也就是Extion ClassLoader,詢問它是否能夠裝載該類,如果能,那AppClassLoader就不干這活了,同樣Extion ClassLoader在裝載時,也會先問問它的父類裝載器。我們可以看出類裝載器實際上是一個樹狀的結(jié)構(gòu)圖,每個類裝載器有自己的父親,類裝載器在裝載類時,總是先讓自己的父類裝載器裝載(多么尊敬長輩),如果父類裝載器無法裝載該類時,自己就會動手裝載,如果它也裝載不了,那么對不起,它會大喊一聲:Exceptionclass not found。有必要提一句,當(dāng)由直接使用類路徑裝載器裝載類失敗拋出的是NoClassDefFoundException異常。如果使用自定義的類裝載器loadClass方法或者ClassLoaderfindSystemClass方法裝載類,如果你不去刻意改變,那么拋出的是ClassNotFoundException

          我們簡短總結(jié)一下上面的討論:

          1.JVM類裝載器的體系結(jié)構(gòu)可以看作是樹狀結(jié)構(gòu)。

          2.父類裝載器優(yōu)先裝載。在父類裝載器裝載失敗的情況下再裝載,如果都裝載失敗則拋出ClassNotFoundException或者NoClassDefFoundError異常。

          那么我們的類在什么情況下被裝載的呢?

          2. 類如何被裝載

          java2中,JVM是如何裝載類的呢,可以分為兩種類型,一種是隱式的類裝載,一種是顯式的類裝載

          2.1 隱式的類裝載

          隱式的類裝載是編碼中最常用得方式:

          A b = new A();

          如果程序運行到這段代碼時還沒有A類,那么JVM會請求裝載當(dāng)前類的類裝器來裝載類。問題來了,我把代碼弄得復(fù)雜一點點,但依舊沒有任何難度,請思考JVM得裝載次序:

          package test;

          Public class A{

              public void static main(String args[]){

                  B b new B();

              }

          }

          class B{C c;}

          class C{}

          揭曉答案,類裝載的次序為A->B,而類C根本不會被JVM理會,先不要驚訝,仔細想想,這不正是我們最需要得到的結(jié)果。我們仔細了解一下JVM裝載順序。當(dāng)使用Java A命令運行A類時,JVM會首先要求類路徑類裝載器(AppClassLoader)裝載A類,但是這時只裝載A,不會裝載A中出現(xiàn)的其他類(B),接著它會調(diào)用A中的main函數(shù),直到運行語句b new B()時,JVM發(fā)現(xiàn)必須裝載B類程序才能繼續(xù)運行,于是類路徑類裝載器會去裝載B類,雖然我們可以看到B中有有C類的聲明,但是并不是實際的執(zhí)行語句,所以并不去裝載C類,也就是說JVM按照運行時的有效執(zhí)行語句,來決定是否需要裝載新類,從而裝載盡可能少的類,這一點和編譯類是不相同的。

          2.2 顯式的類裝載

          使用顯示的類裝載方法很多,我們都裝載類test.A為例。

          使用Class類的forName方法。它可以指定裝載器,也可以使用裝載當(dāng)前類的裝載器。例如:

          Class.forName("test.A");                                            

          它的效果和

          Class.forName("test.A",true,this.getClass().getClassLoader());

          是一樣的。

          使用類路徑類裝載裝載.

          ClassLoader.getSystemClassLoader().loadClass("test.A");

          使用當(dāng)前進程上下文的使用的類裝載器進行裝載,這種裝載類的方法常常被有著復(fù)雜類裝載體系結(jié)構(gòu)的系統(tǒng)所使用。

          Thread.currentThread().getContextClassLoader().loadClass("test.A")

          使用自定義的類裝載器裝載類

          public class MyClassLoader extends URLClassLoader{

          public MyClassLoader() {

                  super(new URL[0]);

              }

          }

          MyClassLoader myClassLoader = new MyClassLoader();

          myClassLoader.loadClass("test.A");

          MyClassLoader繼承了URLClassLoader類,這是JDK核心包中的類裝載器,在沒有指定父類裝載器的情況下,類路徑類裝載器就是它的父類裝載器,MyClassLoader并沒有增加類的查找范圍,因此它和類路徑裝載器有相同的效果。

          我們已經(jīng)知道Java的類裝載器體系結(jié)構(gòu)為樹狀,多個類裝載器可以指定同一個類裝載器作為自己的父類,每個子類裝載器就是樹狀結(jié)構(gòu)的一個分支,當(dāng)然它們又可以個有子類裝載器類裝載器,類裝載器也可以沒有父類裝載器,這時Bootstrap類裝載器將作為它的隱含父類,實際上Bootstrap類裝載器是所有類裝載器的祖先,也是樹狀結(jié)構(gòu)的根。這種樹狀體系結(jié)構(gòu),以及父類裝載器優(yōu)先的機制,為我們編寫自定義的類裝載器提供了便利,同時可以讓程序按照我們希望的方式進行類的裝載。例如某個程序的類裝載器體系結(jié)構(gòu)圖如下:

          2:某個程序的類裝載器的結(jié)構(gòu)

          解釋一下上面的圖,ClassLoaderA為自定義的類裝載器,它的父類裝載器為類路徑裝載器,它有兩個子類裝載器ClassLoaderAAClassLaderABClassLoaderB為程序使用的另外一個類裝載器,它沒有父類裝載器,但有一個子類裝載器ClassLoaderBB。你可能會說,見鬼,我的程序怎么會使用這么復(fù)雜的類裝載器結(jié)構(gòu)。為了進行下面的討論,暫且委屈一下。

          3. 奇怪的隔離性

          我們不難發(fā)現(xiàn),圖2中的類裝載器AAAB ABBBAAB等等位于不同分支下,他們之間沒有父子關(guān)系,我不知道如何定義這種關(guān)系,姑且稱他們位于不同分支下。兩個位于不同分支的類裝載器具有隔離性,這種隔離性使得在分別使用它們裝載同一個類,也會在內(nèi)存中出現(xiàn)兩個Class類的實例。因為被具有隔離性的類裝載器裝載的類不會共享內(nèi)存空間,使得使用一個類裝載器不可能完成的任務(wù)變得可以輕而易舉,例如類的靜態(tài)變量可能同時擁有多個值(雖然好像作用不大),因為就算是被裝載類的同一靜態(tài)變量,它們也將被保存不同的內(nèi)存空間,又例如程序需要使用某些包,但又不希望被程序另外一些包所使用,很簡單,編寫自定義的類裝載器。類裝載器的這種隔離性在許多大型的軟件應(yīng)用和服務(wù)程序得到了很好的應(yīng)用。下面是同一個類靜態(tài)變量為不同值的例子。

          package test;

          public class A {

           public static void main( String[] args ) {

              try {

                //定義兩個類裝載器

                MyClassLoader aa= new MyClassLoader();

                MyClassLoader bb = new MyClassLoader();

                //用類裝載器aa裝載testb.B

                Class clazz=aa.loadClass("testb. B");

                Constructor constructor=

                  clazz.getConstructor(new Class[]{Integer.class});

                Object object =

               constructor.newInstance(new Object[]{new Integer(1)});

                Method method =

               clazz.getDeclaredMethod("printB",new Class[0]);

                //用類裝載器bb裝載testb.B

                Class clazz2=bb.loadClass("testb. B");

                Constructor constructor2 =

                  clazz2.getConstructor(new Class[]{Integer.class});

                Object object2 =

               constructor2.newInstance(new Object[]{new Integer(2)});

                Method method2 =

               clazz2.getDeclaredMethod("printB",new Class[0]);

                //顯示test.B中的靜態(tài)變量的值

                method.invoke( object,new Object[0]);

                method2.invoke( object2,new Object[0]);

              } catch ( Exception e ) {

                e.printStackTrace();

              }

           }

          }

          //Class B 必須位于MyClassLoader的查找范圍內(nèi),

          //而不應(yīng)該在MyClassLoader的父類裝載器的查找范圍內(nèi)。

          package testb;

          public class B {

              static int b ;

              public B(Integer testb) {

                  b = testb.intValue();

              }

              public void printB() {

                  System.out.print("my static field b is ", b);

              }

          }

          public class MyClassLoader extends URLClassLoader{

           private static File file = new File("c:""classes ");

           //該路徑存放著class B,但是沒有class A

           public MyClassLoader() {

              super(getUrl());

           }

           public static URL[] getUrl() {

              try {

                return new URL[]{file.toURL()};

              } catch ( MalformedURLException e ) {

                return new URL[0];

              }

           }

          }

          程序的運行結(jié)果為:

          my static field b is 1

          my static field b is 2

          程序的結(jié)果非常有意思,從編程者的角度,我們甚至可以把不在同一個分支的類裝載器看作不同的java虛擬機,因為它們彼此覺察不到對方的存在。程序在使用具有分支的類裝載的體系結(jié)構(gòu)時要非常小心,弄清楚每個類裝載器的類查找范圍,盡量避免父類裝載器和子類裝載器的類查找范圍中有相同類名的類(包括包名和類名),下面這個例子就是用來說明這種情況可能帶來的問題。

          假設(shè)有相同名字卻不同版本的接口 A

          版本 1

          package test;

          Intefer Same{ public String getVersion(); }

          版本 2

          Package test;

          Intefer Same{ public String getName(); }

          接口A兩個版本的實現(xiàn):

          版本1的實現(xiàn)

          package test;

          public class Same1Impl implements Same {

          public String getVersion(){ return "A version 1";}

          }

          版本2的實現(xiàn)

          public class Same 2Impl implements Same {

          public String getName(){ return "A version 2";}

          }

          我們依然使用圖2的類裝載器結(jié)構(gòu),首先將版本1SameSame的實現(xiàn)類Same1Impl打成包same1.jar,將版本2SameSame的實現(xiàn)類Same1Impl打成包same2.jar。現(xiàn)在,做這樣的事情,把same1.jar放入類裝載器ClassLoaderA的類查找范圍中,把same2.jar放入類裝器ClassLoaderAB的類查找范圍中。當(dāng)你興沖沖的運行下面這個看似正確的程序。

          實際上這個錯誤的是由父類載器優(yōu)先裝載的機制造成,當(dāng)類裝載器ClassLoaderAB在裝載Same2Impl類時發(fā)現(xiàn)必須裝載接口test.Same,于是按規(guī)定請求父類裝載器裝載,父類裝載器發(fā)現(xiàn)了版本1test.Same接口并興沖沖的裝載,但是卻想不到Same2Impl所希望的是版本2 test.Same,后面的事情可想而知了,異常被拋出。

          我們很難責(zé)怪Java中暫時并沒有提供區(qū)分版本的機制,如果使用了比較復(fù)雜的類裝載器體系結(jié)構(gòu),在出現(xiàn)了某個包或者類的多個版本時,應(yīng)特別注意。

          掌握和靈活運用Java的類裝載器的體系結(jié)構(gòu),對程序的系統(tǒng)設(shè)計,程序的實現(xiàn),已經(jīng)程序的調(diào)試,都有相當(dāng)大的幫助。希望以上的內(nèi)容能夠?qū)δ兴鶐椭?/span>

          主站蜘蛛池模板: 民县| 高雄县| 瓦房店市| 象山县| 庄河市| 田东县| 长治县| 进贤县| 湖南省| 富源县| 南和县| 上高县| 新乡市| 沁阳市| 昌乐县| 团风县| 漳州市| 梅河口市| 巴彦淖尔市| 青铜峡市| 马龙县| 西充县| 上饶县| 扎赉特旗| 同仁县| 定陶县| 卢龙县| 内乡县| 乌什县| 泾源县| 繁昌县| 墨竹工卡县| 马关县| 云林县| 灌南县| 武汉市| 防城港市| 定西市| 新源县| 任丘市| 蒙城县|