Java類裝載體系中的隔離性

來源:互聯網
上載者:User

Java類裝載體系中的隔離性

作者:盛戈歆

作者簡介

盛戈歆,軟體工程師,你可以通過shenggexin@topwaver.com與他聯絡。

本文:

Java中類的尋找與裝載出現的問題總是會時不時出現在Java程式員面前,這並不是什麼丟臉的事情,相信沒有一個Java程式員沒遇到過ClassNotException,因此不要為被人瞅見自己也犯這樣的錯誤而覺得不自然,但是在如果出現了ClassNotFoundException後異常後一臉的茫然,那我想你該瞭解一下java的類裝載的體制了,同時為了進行下面的關於類裝載器之間的隔離性的討論,我們先簡單介紹一下類裝載的體繫結構。

1. Java類裝載體繫結構

裝載類的過程非常簡單:尋找類所在位置,並將找到的Java類的位元組碼裝入記憶體,產生對應的Class對象。Java的類裝載器專門用來實現這樣的過程,JVM並不止有一個類裝載器,事實上,如果你願意的話,你可以讓JVM擁有無數個類裝載器,當然這除了測試JVM外,我想不出還有其他的用途。你應該已經發現到了這樣一個問題,類裝載器自身也是一個類,它也需要被裝載到記憶體中來,那麼這些類裝載器由誰來裝載呢,總得有個根吧?沒錯,確實存在這樣的根,它就是神龍見首不見尾的Bootstrap ClassLoader. 為什麼說它神龍見首不見尾呢,因為你根本無法在Java代碼中抓住哪怕是它的一點點的尾巴,儘管你能時時刻刻體會到它的存在,因為java的運行環境所需要的所有類庫,都由它來裝載,而它本身是C++寫的程式,可以獨立運行,可以說是JVM的運行起點,偉大吧。在Bootstrap完成它的任務後,會產生一個AppClassLoader(實際上之前系統還會使用擴充類裝載器ExtClassLoader,它用於裝載Java運行環境擴充包中的類),這個類裝載器才是我們經常使用的,可以調用ClassLoader.getSystemClassLoader() 來獲得,我們假定程式中沒有使用類裝載器相關操作設定或者自訂新的類裝載器,那麼我們編寫的所有java類通通會由它來裝載,值得尊敬吧。AppClassLoader尋找類的地區就是耳熟能詳的Classpath,也是初學者必須跨過的門檻,有沒有靈光一閃的感覺,我們按照它的類尋找範圍給它取名為類路徑類裝載器。還是先前假定的情況,當Java中出現新的類,AppClassLoader首先在類傳遞給它的父類類裝載器,也就是Extion ClassLoader,詢問它是否能夠裝載該類,如果能,那AppClassLoader就不幹這活了,同樣Extion ClassLoader在裝載時,也會先問問它的父類裝載器。我們可以看出類裝載器實際上是一個樹狀的結構圖,每個類裝載器有自己的父親,類裝載器在裝載類時,總是先讓自己的父類裝載器裝載(多麼尊敬長輩),如果父類裝載器無法裝載該類時,自己就會動手裝載,如果它也裝載不了,那麼對不起,它會大喊一聲:Exception,class not found。有必要提一句,當由直接使用類路徑裝載器裝載類失敗拋出的是NoClassDefFoundException異常。如果使用自訂的類裝載器loadClass方法或者ClassLoader的findSystemClass方法裝載類,如果你不去刻意改變,那麼拋出的是ClassNotFoundException。

我們簡短總結一下上面的討論:

1.JVM類裝載器的體繫結構可以看作是樹狀結構。

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

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

2. 類如何被裝載

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

2.1 隱式的類裝載

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

A b = new A();

如果程式運行到這段代碼時還沒有A類,那麼JVM會請求裝載當前類的類裝器來裝載類。問題來了,我把代碼弄得複雜一點點,但依舊沒有任何難度,請思考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理會,先不要驚訝,仔細想想,這不正是我們最需要得到的結果。我們仔細瞭解一下JVM裝載順序。當使用Java A命令運行A類時,JVM會首先要求類路徑類裝載器(AppClassLoader)裝載A類,但是這時只裝載A,不會裝載A中出現的其他類(B類),接著它會調用A中的main函數,直到運行語句b = new B()時,JVM發現必須裝載B類程式才能繼續運行,於是類路徑類裝載器會去裝載B類,雖然我們可以看到B中有有C類的聲明,但是並不是實際的執行語句,所以並不去裝載C類,也就是說JVM按照運行時的有效執行語句,來決定是否需要裝載新類,從而裝載儘可能少的類,這一點和編譯類是不相同的。

2.2 顯式的類裝載

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

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

Class.forName("test.A");
它的效果和
Class.forName("test.A",true,this.getClass().getClassLoader());
是一樣的。

使用類路徑類裝載裝載.

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

使用當前進程內容相關的使用的類裝載器進行裝載,這種裝載類的方法常常被有著複雜類裝載體繫結構的系統所使用。

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並沒有增加類的尋找範圍,因此它和類路徑裝載器有相同的效果。

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

 

圖2:某個程式的類裝載器的結構

解釋一下上面的圖,ClassLoaderA為自訂的類裝載器,它的父類裝載器為類路徑裝載器,它有兩個子類裝載器ClassLoaderAA和ClassLaderAB,ClassLoaderB為程式使用的另外一個類裝載器,它沒有父類裝載器,但有一個子類裝載器ClassLoaderBB。你可能會說,見鬼,我的程式怎麼會使用這麼複雜的類裝載器結構。為了進行下面的討論,暫且委屈一下。

3. 奇怪的隔離性

我們不難發現,圖2中的類裝載器AA和AB, AB和BB,AA和B等等位於不同分支下,他們之間沒有父子關係,我不知道如何定義這種關係,姑且稱他們位於不同分支下。兩個位於不同分支的類裝載器具有隔離性,這種隔離性使得在分別使用它們裝載同一個類,也會在記憶體中出現兩個Class類的執行個體。因為被具有隔離性的類裝載器裝載的類不會共用記憶體空間,使得使用一個類裝載器不可能完成的任務變得可以輕而易舉,例如類的靜態變數可能同時擁有多個值(雖然好像作用不大),因為就算是被裝載類的同一靜態變數,它們也將被儲存不同的記憶體空間,又例如程式需要使用某些包,但又不希望被程式另外一些包所使用,很簡單,編寫自訂的類裝載器。類裝載器的這種隔離性在許多大型的軟體應用和服務程式得到了很好的應用。下面是同一個類靜態變數為不同值的例子。

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中的靜態變數的值
      method.invoke( object,new Object[0]);
      method2.invoke( object2,new Object[0]);
    } catch ( Exception e ) {
      e.printStackTrace();
    }
  }
}

 

//Class B 必須位於MyClassLoader的尋找範圍內,
//而不應該在MyClassLoader的父類裝載器的尋找範圍內。
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];
    }
  }
}

程式的運行結果為:

my static field b is 1
my static field b is 2

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

假設有相同名字卻不同版本的介面 A,

版本 1:
package test;
Intefer Same{ public String getVersion(); }
版本 2:
Package test;
Intefer Same{ public String getName(); }

介面A兩個版本的實現:

版本1的實現
package test;
public class Same1Impl implements Same {
public String getVersion(){ return "A version 1";}
}
版本2的實現
public class Same 2Impl implements Same {
public String getName(){ return "A version 2";}
}

我們依然使用圖2的類裝載器結構,首先將版本1的Same和Same的實作類別Same1Impl打成包same1.jar,將版本2的Same和Same的實作類別Same1Impl打成包same2.jar。現在,做這樣的事情,把same1.jar放入類裝載器ClassLoaderA的類尋找範圍中,把same2.jar放入類裝器ClassLoaderAB的類尋找範圍中。當你興沖沖的運行下面這個看似正確的程式。

實際上這個錯誤的是由父類載器優先裝載的機製造成,當類裝載器ClassLoaderAB在裝載Same2Impl類時發現必須裝載介面test.Same,於是按規定請求父類裝載器裝載,父類裝載器發現了版本1的test.Same介面並興沖沖的裝載,但是卻想不到Same2Impl所希望的是版本2 的test.Same,後面的事情可想而知了,異常被拋出。

我們很難責怪Java中暫時並沒有提供區分版本的機制,如果使用了比較複雜的類裝載器體繫結構,在出現了某個包或者類的多個版本時,應特別注意。

掌握和靈活運用Java的類裝載器的體繫結構,對程式的系統設計,程式的實現,已經程式的調試,都有相當大的協助。希望以上的內容能夠對您有所協助。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.