Java类别载入器

1 Java的动态特性

  Java的动态特性有两种,一是隐式的;另一种是显示的。隱式的(implicit)方法就是當程式設計師用到new 這個Java 關鍵字時,會讓類別載入器依需求載入您所需要的類別,這種方式使用了隱式的(implicit)方法。顯式的方法,又分成兩種方式,一種是藉由java.lang.Class 裡的forName()方法,另一種則是藉由java.lang.ClassLoader 裡的loadClass()方法。您可以任意選用其中一種方法。

2 隐式的动态特性

  在执行java文件时,只有单独的变量声明是不会载入相应的类的,只有在用new生成实例时才载入

 如示例所示:

public class Main

public static void main(String args[])

{

A a1 = new A() ;

B b1 ;

}

A和B相同,如下:

public class A

{

    public void print(using A);

}

编译后,可用java verbose:class Main运行,察看输出结果。可以看到JVM只载入了A,而没有载入B.

另外,类的载入只在执行到new一个类时,才载入,如果没有执行到new语句,则不载入。

如://类Office

public class Office

{

   public static void main(String[] args)

   {

   Word myword=null;

  

   Excel myexcel=null;

  

   if (args[0].equals("Word"))

   {

             myword = new Word();

             myword.start();    

   }

         if (args[0].equals("Excel"))

         {

             myexcel = new Excel();

             myexcel.start();       

         } 

  

  

   }

}

//类Word和Excel基本相同,如下

public class Word

{

   public void start()

   {

   System.out.println("using word");

   }

}

dos命令提示符下,输入java verbose Office Excel可以看到JVM只载入Excel类,而不载入Word类。

 

 

 

3 显示的动态特性

3.1 java.lang.Class里的forName()方法

在上一个Office示例中,进行如下修改:

一 加入Assembly类

public interface Assembly

{

    public void start();

}

二 让Word和Excel类实现该接口

public class Word implements Assembly

{

   public void start()

   {

   System.out.println("using word");

   }

}

Office 类如下所示

public class Office

{

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

   {

   java.lang.Class c = java.lang.Class.forName(args[0]);   

   Object o = c.newInstance();

   Assembly a = (Assembly)o;

   a.start();

   

   }

}

在命令提示符下输入java verbose Office Word 输出入下:

通过上图你可以看到,interface 如同class 一般,會由編譯器產生一個獨立的類別檔(.class),當類別載入器載入類別時,如果發現該類別繼承了其他類別,或是實作了其他介面,就會先載入代表該介面的類別檔,也會載入其父類別的類別檔,如果父類別也有其父類別,也會一併優先載入。換句話說,類別載入器會依繼承體系最上層的類別往下依序載入,直到所有的祖先類別都載入了,才輪到自己載入。

下面介绍一下 forName 函数, 如果您親自搜尋Java 2 SDK 說明檔內部對於Class 這個類別的說明,您可以發現其實有兩個forName()方法,一個是只有一個參數的(就是之前程式之中所使用的):

public static Class forName(String className)

另外一個是需要三個參數的:

public static Class forName(String name, boolean initialize,ClassLoader loader)

這兩個方法,最後都是連接到原生方法forName0(),其宣告如下:

private static native Class forName0(String name, boolean initialize, ClassLoader loader)

throws ClassNotFoundException;

只有一個參數的forName()方法,最後叫用的是:

forName0(className, true, ClassLoader.getCallerClassLoader());

而具有三個參數的forName()方法,最後叫用的是:

forName0(name, initialize, loader);

这里initialize参数指,在载入类之后是否进行初始化,对于该参数的作用可用如下示例察看:

类里的静态初始化块在类第一次被初始化时才被呼叫,且仅呼叫一次。在Word类里,加入静态初始化块

public class Word implements Assembly

{

   static

   {

   System.out.println("word static initialization "); 

   }

   

   public void start()

   {

   System.out.println("using word");

   }

}

将类Office作如下改变:

public class Office

{

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

   {

   Office off= new Office();

   System.out.println("类别准备载入");

   java.lang.Class c = java.lang.Class.forName(args[0],true,off.getClass().getClassLoader());     

   System.out.println("类别准备实体化");

   Object o  = c.newInstance();

   Object o2 = c.newInstance();

   

   }

}

如果第二个参数为true 则输出入下

如果为false ,则输出入下:

可见,类里的静态初始化块仅在初始化时才执行,且不过初始化几次,它仅执行一次(这里有一个条件,那就是只有它是被同一个类别载入器多次载入时,才是这样,如果被不同的载入器,载入多次,则静态初始化块会执行多次)。

关于第三个参数请见下节介绍

3.2 直接使用类别载入器 java.lang.ClassLoader

Java 之中,每個類別最後的老祖宗都是Object,而Object 裡有一個名為getClass()的方法,就是用來取得某特定實體所屬類別的參考,這個參考,指向的是一個名為Class 類別(Class.class) 的實體,您無法自行產生一個Class 類別的實體,因為它的建構式被宣告成private,這個Class 類別的實體是在類別檔(.class)第一次載入記憶體時就建立的,往後您在程式中產生任何該類別的實體,這些實體的內部都會有一個欄位記錄著這個Class 類別的所在位置。

基本上,我們可以把每個Class 類別的實體,當作是某個類別在記憶體中的代理人。每次我們需要

查詢該類別的資料(如其中的fieldmethod )時,就可以請這個實體幫我們代勞。事實上,JavaReflection 機制,就大量地利用Class 類別。去深入Class 類別的原始碼,我們可以發現Class類別的定義中大多數的方法都是原生方法(native method)

Java 之中,每個類別都是由某個類別載入器(ClassLoader 的實體)來載入,因此,Class 類別的實體中,都會有欄位記錄著載入它的ClassLoader 的實體(注意:如果該欄位是null,並不代表它不是由類別載入器所載入,而是代表這個類別由靴帶式載入器(bootstrap loader,也有人稱rootloader)所載入,只不過因為這個載入器並不是用Java 所寫成,是用C++写的,所以邏輯上沒有實體)

    系統裡同時存在多個ClassLoader 的實體,而且一個類別載入器不限於只能載入一個類別,類別載入器可以載入多個類別。所以,只要取得Class 類別實體的參考,就可以利用其getClassLoader()方法籃取得載入該類別之類別載入器的參考。getClassLoader()方法最後會呼叫原生方法getClassLoader0(),其宣告如下:private native ClassLoader getClassLoader0();

最後,取得了ClassLoader 的實體,我們就可以叫用其loadClass()方法幫我們載入我們想要的類别,因此上面的Office类可做如下修改:

public class Office

{

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

   {

   Office off= new Office();

   System.out.println("类别准备载入");

    ClassLoader loader = off.getClass().getClassLoader();

   java.lang.Class c = loader.loadClass(args[0]);

   System.out.println("类别准备实体化");

   Object o  = c.newInstance();

   Object o2 = c.newInstance();

   }

}

其输出结果同forName方法的第二个参数为false时相同。可见载入器载入类时只进行载入,不进行初始化。

获取ClassLoader还可以用如下的方法:

public class Office

{

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

   {

    java.lang.Class cb = Office.class; 

   System.out.println("类别准备载入");

   ClassLoader loader = cb.getClassLoader();

   java.lang.Class c = loader.loadClass(args[0]);

   System.out.println("类别准备实体化");

   Object o  = c.newInstance();

   Object o2 = c.newInstance();

   }

}

在此之前,當我們談到使用類別載入器來載入類別時,都是使用既有的類別載入器來幫我們載

入我們所指定的類別。那麼,我們可以自己產生類別載入器來幫我們載入類別嗎? 答案是肯定的。

利用Java 本身提供的java.net.URLClassLoader 類別就可以做到。

   public class Office

{

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

   {

      URL u = new URL("file:/d:/myapp/classload/");

        URLClassLoader ucl = new URLClassLoader(new URL[]{u});

        java.lang.Class c = ucl.loadClass(args[0]);

        Assembly asm = (Assembly)c.newInstance();

        asm.start();

   }   

}

在這個範例中,我們自己產生java.net.URLClassLoader 的實體來幫我們載入我們所需要的類別。但是載入前,我們必須告訴URLClassLoader 去哪個地方尋找我們所指定的類別才行,所以我們必須給它一個URL 類別所構成的陣列,代表我們希望它去搜尋的所有位置。URL 可以指向網際網路上的任何位置,也可以指向我們電腦裡的檔案系統(包含JAR )。在上述範例中,我們希望URLClassLoader d:/my/lib/ 這個目錄下去尋找我們需要的類別, 所以指定的URL為”file:/d:/my/lib/”。其實,如果我們請求的位置是主要類別(public static void main(String args[])方法的那個類別)的相對目錄,我們可以在URL 的地方只寫”file:lib/”,代表相對於目前的目錄。

下面我们来看一下系统为我们提供的3个类别载入器:

java.exe 是利用幾個基本原則來尋找Java Runtime Environment(JRE),然後把類別檔(.class)直接轉交給JRE 執行之後,java.exe 就功成身退。類別載入器也是構成JRE 的其中一個重要成員,所以最後類別載入器就會自動從所在之JRE 目錄底下的/lib/rt.jar 載入基礎類別函式庫。

當我們在命令列輸入java xxx.class 的時候,java.exe 根據我們之前所提過的邏輯找到了JRE(Java Runtime Environment),接著找到位在JRE 之中的jvm.dll(真正的Java 虛擬機器),最後載入這個動態聯結函式庫,啟動Java 虛擬機器。虛擬機器一啟動,會先做一些初始化的動作,比方說抓取系統參數等。一旦初始化動作完成之後,就會產生第一個類別載入器,即所謂的Bootstrap LoaderBootstrap Loader 是由C++所撰寫而成(所以前面我們說,以Java 的觀點來看,邏輯上並不存在Bootstrap Loader 的類別實體,所以在Java 程式碼裡試圖印出其內容的時候,我們會看到的輸出為null),這個Bootstrap Loader

做的初始工作中,除了也做一些基本的初始化動作之外,最重要的就是載入定義在sun.misc 命名空間底下的Launcher.java 之中的ExtClassLoader(因為是inner class,所以編譯之後會變成Launcher$ExtClassLoader.class),並設定其Parent 為null,代表其父載入器為BootstrapLoader。然後Bootstrap Loader 再要求載入定義於sun.misc 命名空間底下的Launcher.java 之中的AppClassLoader(因為是inner class,所以編譯之後會變成Launcher$AppClassLoader.class),並設定其Parent 為之前產生的ExtClassLoader 實體。

這裡要請大家注意的是,Launcher$ExtClassLoader.class 與Launcher$AppClassLoader.class 都可能是由Bootstrap Loader 所載入,所以Parent 和由哪個類別載入器載入沒有關係。

三个载入器的层次关系可通过运行下面的例子察看:

public class Test

{

    public static void main(String[] args)

    {

        ClassLoader cl1 = Test.class.getClassLoader();

        System.out.println(cl1);

        ClassLoader cl2 = cl1.getParent();

        System.out.println(cl2);

        ClassLoader cl3 = cl2.getParent();

        System.out.println(cl3);

    }

}

运行结果:

sun.misc.Launcher$AppClassLoader@1a0c10f

sun.misc.Launcher$ExtClassLoader@e2eec8

null

//

如果在上述程式中,如果您使用程式碼:

cl1.getClass.getClassLoader()及cl2.getClass.getClassLoader(),您會發現印出的都是null,

這代表它們都是由Bootstrap Loader 所載入。這裡也再次強調,類別載入器由誰載入(這句話有點

詭異,類別載入器也要由類別載入器載入,這是因為除了Bootstrap Loader 之外,其餘的類別載

入器皆是由Java 撰寫而成),和它的Parent 是誰沒有關係,Parent 的存在只是為了某些特殊目的,

這個目的我們將在稍後作解釋。

在此要請大家注意的是,AppClassLoader 和ExtClassLoader 都是URLClassLoader 的子類別。

由於它們都是URLClassLoader 的子類別,所以它們也應該有URL 作為搜尋類別檔的參考,由原始碼

中我們可以得知,AppClassLoader 所參考的URL 是從系統參數java.class.path 取出的字串所決定,

java.class.path 則是由我們在執行java.exe 時,利用–cp 或-classpath 或CLASSPATH 環境變

數所決定。

用如下示例测试:

public class AppLoader

{

    public static void main(String[] args)

    {

        String s = System.getProperty("java.class.path");

        System.out.println(s);

    }

}

/

D:/myapp/classload>java AppLoader

.;D:/myjava/Tomcat5.0/webapps/axis/WEB-INF/lib/axis.jar;D:/myjava/Tomcat5.0/weba

pps/axis/WEB-INF/lib/commons-logging.jar;D:/myjava/Tomcat5.0/webapps/axis/WEB-IN

F/lib/commons-discovery.jar;C:/oracle/ora81/jdbc/lib/classes12.zip;D:/myjava/JDB

CforSQLserver/lib/mssqlserver.jar;D:/myjava/JDBCforSQLserver/lib/msbase.jar;D:/m

yjava/JDBCforSQLserver/lib/msutil.jar;D:/myjava/Tomcat5.0/common/lib/servlet-api

.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/rt.jar;C:/sun/appserver/lib/j2ee.jar;D:/myj

ava/j2sdk1.4.2_04/lib/jaxp.jar;D:/myjava/j2sdk1.4.2_04/lib/sax.jar;

 

 

D:/myapp/classload>java -classpath .;d:/myapp AppLoader

.;d:/myapp

/

從這個輸出結果,我們可以看出,在預設情況下,AppClassLoader 的搜尋路徑為”.(目前所在目

),如果使用-classpath 選項(與-cp 等效),就可以改變AppClassLoader 的搜尋路徑,如果沒有

指定-classpath 選項,就會搜尋環境變數CLASSPATH。如果同時有CLASSPATH 的環境設定與

-classpath 選項,則以-classpath 選項的內容為主,CLASSPATH 的環境設定與-classpath 選項兩者

的內容不會有加成的效果。

至於ExtClassLoader 也有相同的情形,不過其搜尋路徑是參考系統參數java.ext.dirs。

系統參數java.ext.dirs 的內容,會指向java.exe 所選擇的JRE 所在位置下的/lib/ext 子目錄。Java.exe使用的JRE是在系统变量path里指定的,可以通过修改path从而修改ExtCLassLoader的搜寻路径,也可以如下命令参数來更改,

java Djava.ext.dirs=c:/winnt/ AppLoader //注意 =号两边不能有空格。-D也不能和java分开。

 

 

D:/myapp/classload>java ExtLoader

D:/myjava/j2sdk1.4.2_04/jre/lib/ext

D:/myapp/classload>java -Djava.ext.dirs=c:/winnt/ ExtLoader

c:/winnt/

最後一個類別載入器是Bootstrap Loader , 我們可以經由查詢由系統參數sun.boot.class.path 得知Bootstrap Loader 用來搜尋類別的路徑。该路径的修改与ExtClassLoader的相同。但修改后不影响Bootstrap的搜寻路径。

在命令列下參數時,使用–classpath / -cp / 環境變數CLASSPATH 來更改AppClassLoader

的搜尋路徑,或者用–Djava.ext.dirs 來改變ExtClassLoader 的搜尋目錄,兩者都是有意義的。

可是用–Dsun.boot.class.path 來改變Bootstrap Loader 的搜尋路徑是無效。這是因為

AppClassLoader 與ExtClassLoader 都是各自參考這兩個系統參數的內容而建立,當您在命令列下

變更這兩個系統參數之後, AppClassLoader ExtClassLoader 在建立實體的時候會參考這兩個系

統參數,因而改變了它們搜尋類別檔的路徑;而系統參數sun.boot.class.path 則是預設與

Bootstrap Loader 的搜尋路徑相同,就算您更改該系統參與,與Bootstrap Loader 完全無關。

改变java.exe所使用的jre会改变Bootstrap Loader的搜寻路径。

Bootstrap Loader的搜寻路径一般如下:

///

D:/myjava/j2sdk1.4.2_04/jre/lib/rt.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/i18n.jar;

D:/myjava/j2sdk1.4.2_04/jre/lib/sunrsasign.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/j

sse.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/jce.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/

charsets.jar;D:/myjava/j2sdk1.4.2_04/jre/classes

///

更重要的是,AppClassLoader 與ExtClassLoader 在整個虛擬機器之中只會存有一份,一旦建

立了,其內部所參考的搜尋路徑將不再改變,也就是說,即使我們在程式裡利用System.setProperty()

來改變系統參數的內容,仍然無法更動AppClassLoader 與ExtClassLoader 的搜尋路徑。因此,執

行時期動態更改搜尋路徑的設定是不可能的事情。如果因為特殊需求,有些類別的所在路徑並非在

一開始時就能決定,那麼除了產生新的類別載入器來輔助我們載入所需的類別之外,沒有其他方法了。

 

 

下面我们将看一下载入器的委派模型

所謂的委派模型,用簡單的話來講,就是「類別載入器有載入類別的需求時,會先請示其Parent 使用其搜尋路徑幫忙載入,如果Parent 找不到,那麼才由自己依照自己的搜尋路徑搜尋類別」。

下面我们看一下小的示例:

public class Test

{

    public static void main(String[] args)

    {

       System.out.println(Test.class.getClassLoader());

       TestLib tl = new TestLib();

       tl.start();

    }

}

public class TestLib

{

    public void start()

    {

       System.out.println(this.getClass().getClassLoader());

    }

}

如果这两个类仅放在dos命令提示符的当前目录下,则输出结果如下:

//

sun.misc.Launcher$AppClassLoader@1a0c10f

sun.misc.Launcher$AppClassLoader@1a0c10f

//

如果这两个类同时又放在<JRE 所在目錄>/lib/ext/classes 底下(在我的机器上是:D:/myjava/j2sdk1.4.2_04/jre/lib/ext/classes,classes没有,需要自己建),输出结果如下:

/

sun.misc.Launcher$ExtClassLoader@e2eec8

sun.misc.Launcher$ExtClassLoader@e2eec8

最后如果在<JRE 所在目錄>/classes下放入这两个类,则输出结果为

/

null

null

如果把<JRE 所在目錄>/classes下的TestLib删去,则输出入下:

//

null

Exception in thread "main" java.lang.NoClassDefFoundError: TestLib

        at Test.main(Test.java:7)

//

这是因为Test的classLoader是Bootstrap Loader ,因此TestLib的也默认为是Bootstrap Loader。Bootstrap Loader搜寻路径下的TestLib被删去了,Bootstrap Loader又没有parent,所以提示找不到。

其他的情况可以自己逐个添加或删除文件,然后执行java Test进行测试,察看输出结果。

 

 

AppClassLoader 與Bootstrap Loader會搜尋它們所指定的位置(或JAR 檔),如果找不到就找不到了,AppClassLoader 與Bootstrap Loader不會遞迴式地搜尋這些位置下的其他路徑或其他沒有被指定的JAR 檔。反觀ExtClassLoader,所參考的系統參數是java.ext.dirs,意思是說,他會搜尋底下的所有JAR 檔以及classes 目錄,作為其搜尋路徑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值