歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Java類與類加載器

虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放在Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱為“類加載器”。

類加載器應用在:類層次劃分、OSGi、熱部署、代碼加密等領域。

類與類加載器

對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。

public class ClassLoaderTest {

 public static void main(String[] args) throws Exception{

  ClassLoader myLoader = new ClassLoader(){

   @Override
   public Class<?> loadClass(String name)
     throws ClassNotFoundException {
    try {

     String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
     InputStream is = getClass().getResourceAsStream(fileName);
     if(is==null){
      return super.loadClass(name);
     }
     
     byte[] b = new byte[is.available()];
     is.read(b);
     return defineClass(name,b,0,b.length);
   
    } catch (IOException e) {
     throw new ClassNotFoundException(name);
    }
   }
   
  };
 
  Object obj = myLoader.loadClass("com.login.ClassLoaderTest").newInstance();
  System.out.println(obj.getClass());
 
  System.out.println(obj instanceof com.login.ClassLoaderTest);
 }

}

運行結果:

class com.login.ClassLoaderTest
false

運行結果中可以看出:

1)這個對象確實是類com.login.ClassLoaderTest實例化出來的對象

2)返回false是因為:虛擬機中存在了兩個ClassLoaderTest類,一個是由系統應用程序類加載器加載的,另外一個是由我們自定義的類加載器加載的,雖然都來自同一個Class文件,但依然是兩個獨立的類,做對象所屬類型檢查時結果自然為false。

雙親委派模型
從Java虛擬機角度來講,只存在兩種不同的類加載器:

1)啟動類加載器(Bootstrap ClassLoader):使用C++語言實現,虛擬機自身的一部分

2)所有其它的類加載器:由Java語言實現,獨立於虛擬機外部,全部繼承自抽象類java.lang.ClassLoader

從Java開發人員的角度看,類加載器還可以更細劃分,絕大部分Java程序會使用到如下:

1)啟動類加載器(Bootstrap ClassLoader):負責將存放在<JAVA_HOME>/lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用null代替即可。

ClassLoader.getClassLoader()方法的代碼片段:

    /**
    * Returns the class loader for the class.  Some implementations may use
    * null to represent the bootstrap class loader. This method will return
    * null in such implementations if this class was loaded by the bootstrap
    * class loader.
    */
    @CallerSensitive
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }

2)擴展類加載器(Extension ClassLoader):這個類加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>/lib/ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。

3)應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$App-ClassLoader實現。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

類加載器的雙親委派模型(Parents Delegation Model):

雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。這裡類加載器直接的父子關系一般不會以繼承(Inheritance)的關系來實現,而是都使用組合(Composition)關系來復用父類加載器的代碼。它並不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。

雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

優點:Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放在rt.jar之中,無論哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,並放在程序的ClassPath中,那系統中將會出現多個不同的Object類,Java類型體系中最基礎的行為也就無法保證,應用程序也將變得一片混亂。可以嘗試去編寫一個與rt.jar類庫中已有類重名的Java類,將會發現可以正常編譯,但是永遠無法被加載運行。

實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            <span >// 首先,檢查請求的類是否已經被加載過了
</span>            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    <span >// 如果父類加載器拋出ClassNotFoundException
                    // 說明父類加載器無法完成加載請求</span>
                }

                if (c == null) {
                    <span >// 在父類加載器無法加載的時候
                    // 在調用本身的findClass方法來進行類加載</span>
                    long t1 = System.nanoTime();
                    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()方法,若父類加載器為空則默認使用啟動類加載器作為父類加載器。如果父類加載失敗,拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。

破壞雙親委派模型
使用了“被破壞”這個詞來形容不符合雙親委派模型原則的行為,這裡“被破壞”並不帶有貶義色彩。

雙親委派模型出現過3次較大規模的“被破壞”情況:

1)第一次破壞發生在雙親委派模型出現之前--JDK1.2發布之前。由於雙親委派模型在JDK1.2才被引入,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,面對已經存在的用戶自定義類加載器的實現代碼,Java設計者引入雙親委派模型時不得不做出一些妥協。為了向前兼容,JDK1.2之後的java.lang.ClassLoader添加了一個新的protected方法findClass(),���此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法, 因為虛擬機在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調用自己的loadClass()。loadClass代碼如上,JDK1.2之後以不提倡用戶再去覆蓋loadClass()方法,而應當把自己的類加載邏輯寫到findClass方法中,在loadClass()方法的邏輯裡如果父類加載失敗,則會調用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符號雙親委派規則的。

2)第二次破壞是由這個模型自身的缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以稱為“基礎”,是因為它們總是作為被用戶代碼調用的API,但世事往往沒有絕對的完美,如果基礎類又要調用回用戶的代碼,那該怎麼辦?

一個典型的例子便是JNDI服務,JNDI現在已經是Java的標准服務,它的代碼由啟動類加載器去加載(在JDK1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,但是啟動類加載器不可能“認識”這些代碼啊!那該怎麼辦?

為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。

有了線程上下文類加載器,就可以做一些“舞弊”的事情了,JNDI服務使用這個上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成加載動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

3)第三次破壞是由於用戶對程序動態性的追求導致的,這裡所說的“動態性”指的是當前一些非常“熱門”的名詞:代碼熱替換(HotSwap)、模塊熱部署(Hot Deloyment)等。

目前OSGi已經成為了業界“事實上”的Java模塊化標准,而OSGi實現模塊化熱部署的關鍵則是它自定義的類加載機制的實現。每一個程序模塊(OSGi中稱為Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起替換掉以實現代碼的熱替換。

在OSGi環境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加復雜的網狀結構,當收到類加載請求時,OSGi將按照下面的順序進行類搜索:

1)將以java.*開頭的類委派給父類加載器加載。

2)否則,將委派列表名單內的類委派給父類加載器加載。

3)否則,將Import列表中的類委派給Export這個類的Bundle的類加載器加載。

4)否則,查找當前Bundle的ClassPath,使用自己的類加載器加載。

5)否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載。

6)否則,查找Dynamic Import列表的Bundle,委派給對應Bundle的類加載器加載。

7)否則,類查找失敗。

上面的查找順序中只有開頭兩點仍然符合雙親委派規則,其余的類查找都是在平級的類加載器中進行的。

Copyright © Linux教程網 All Rights Reserved