`
talentkep
  • 浏览: 98581 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

Class loader 完全手册

阅读更多

學習 Java 的朋友一定都知道,Java 是一種天生就具有動態連結能力的技
術。Java 把每個類別的宣告、介面的宣告,在編譯器處理之後,全部變成一個
個小的執行單位(類別檔, .class),一旦我們指定一個具有public static void
main(String args[])方法的類別作為起點開始運作之後,Java 虛擬機器會找
出所有在執行時期需要的執行單位,並將他們載入記憶體之中,彼此互相交互運
作。儘管本質上是一堆類別檔,但是在記憶體之中,變成了一個”邏輯上”為一體
的Java 應用程式。所以,嚴格的來說,每個類別檔對Java 虛擬機器來說,都
是一個獨立的動態聯結函式庫,只不過它的副檔名不是 .dll 或 .so,而是 .class
罷了。因為這種特性,所以我們可以在不重新編譯其他Java 程式碼的情況下,
只修改有問題的執行單位,並放入檔案系統之中,等到下次該Java 虛擬機器重
新啟動時,這個邏輯上的Java 應用程式就會因為載入了新修改的 .class 檔,
自己的功能也做了更新。這是一個最基本的動態性功能。
但是,如果您用過支援 JSP/Servlet 的高檔Web Server(或稱Web
Container),或是高檔的Application Server 裡的EJB Container,他們一定
會提供一個名為Hot Deployment 的功能,這個功能的意思是說,您可以在
Web Server 不關閉的情況下,放入已經編譯好的新servlet 以取代舊的
servlet,下一次的HTTP request 時,就會自動釋放舊的servlet 所代表的類別
檔,而重新載入新的servlet 所代表的類別檔,同理,如果主角換成EJB
Container,Hot Deployment 可以讓元件部署者不用關閉Application
Server,就能夠將舊的EJB(也是一堆類別檔的集合)換成新版的EJB。
Hot Deployment 的功能並非每家廠商都能提供,只有還算高檔的產品才
有。接下來眼尖的朋友就會問了:「慢著,前面說新的類別必須要等到”下次該
Java 虛擬機器重新啟動時”才會重新載入,可是有些Web Server 或EJB
Container 本身就是用Java 所撰寫,他們也是在Java 虛擬機器上面執行,那
麼,他們如何在Java 虛擬機器不重新啟動的情況下具有Hot Deployment 的
功能? 好吧!就算可以做到讀取新版本的功能好了,在不關閉Java 虛擬機器的
情況下,類別所佔用的記憶體將無法釋放,那麼記憶體裡頭肯定充滿了同一個類
別的新舊版本,如果Hot Deployment 的次數太多,記憶體不會爆掉嗎?」。這
是一個非常好的問題,也是本章之所以存在的理由。
了解了類別載入器的來龍去脈,您將可以讓您的程式具有強大的動態性 -
在Java 虛擬機器不重新啟動的情況下做出具有載入最新類別檔的功能;不關閉
Java 虛擬機器的情況下,釋放類別所佔用的記憶體,讓記憶體不會因為充滿了
同一個類別的多個版本而面臨記憶體不足的窘境。
█我們在不知不覺中用到動態性
3
假設我們撰寫了三個 Java 類別,分別是Main、A、以及B。他們的程式
碼分別如下所示:
檔案:A.java
public class A
{
public void print()
{
System.out.println("Using Class A") ;
}
}
檔案:B.java
public class B
{
public void print()
{
System.out.println("Using Class B") ;
}
}
檔案:Main.java
public class Main
{
public static void main(String args[])
{
A a1 = new A() ;
a1.print() ;
B b1 = new B() ;
b1.print() ;
}
}
這三個檔案編譯好之後,所在目錄的內容如下圖所示:
4
接著,我們執行指令:
java –verbose:class Main
螢幕上會產生一長串的輸出結果,我們擷取最後的部分呈現出來,如下圖所示:
螢幕上的輸出告訴我們,類別載入器(class loader)在背景偷偷的運作,除了將
Java 程式運作時所需要的基礎類別函式庫(又叫核心類別函式庫, Core
Classes)載入記憶體(位於<JRE 所在位置>\lib\rt.jar 之中),我們的程式所用到
的類別Main.class、A.class、以及B.class 也都偷偷被類別載入器載入了記憶
體之中。類別載入器的功用,就是把類別從靜態的硬碟裡(.class 檔),複製一份
放到記憶體之中,並做一些初始化的工作,讓這個類別”活起來”,其他人就能夠
使用它的功能。
有關於基礎類別函式庫,在第一章的時候曾經提到,java.exe 是利用幾個
基本原則來尋找Java Runtime Environment(JRE),然後把類別檔(.class)直接
轉交給JRE 執行之後,java.exe 就功成身退。類別載入器也是構成JRE 的其中
5
一個重要成員,所以最後類別載入器就會自動從所在之 JRE 目錄底下的
\lib\rt.jar 載入基礎類別函式庫。所以在上圖裡,一定是因為java.exe 定位到
c:\j2sdk1.4.0\jre,所以才會有此輸出結果。因此,如果java.exe 定位到其他
的JRE,輸出結果就會有稍許的不同,如下圖所示:
上圖就是 java.exe 定位到位於C:\Program Files\Java\j2re1.4.0 這個JRE
時, 所輸出的結果, 各位可以看到, 類別載入器改由從C:\Program
Files\Java\j2re1.4.0\lib\rt.jar 之中載入。
█預先載入與依需求載入
上述的螢幕輸出還透露了一個訊息,就是 - 我們自己所撰寫的類別(A.class
與B.class)只會在”用到”的時候才載入(Main.class 是起始類別,所以一定比
A.class 和B.class 優先載入),而不是像基礎類別函式庫(位於rt.jar 之中的類別)
一次一股腦地全部載入記憶體之中,所以螢幕上才會輸出先載入了A.class,然
後印出”Using Class A”,再印出載入了B.class 的訊息,然後再印出”Using Class
B”。
像基礎類別函式庫這樣的載入方法我們叫做預先載入(pre-loading),這是
因為基礎類別函式庫裡頭的類別大多是Java 程式執行時所必備的類別,所以為
了不要老是做浪費時間的I/O 動作(讀取檔案系統,然後將類別檔載入記憶體之
中),預先載入這些類別會讓Java 應用程式在執行時速度稍微快一些。相對來
說,我們自己所撰寫的類別之載入方式,叫做依需求載入(load-on-demand),
也就是Java 程式真正用到該類別的時候,才真的把類別檔從檔案系統之中載入
記憶體。
看到上述的說明,大家就會立刻有了結論:『只要參考到特定類別,類別載
入器就會自動幫我們載入類別。』這個結論對嗎? 我們來試試修改後的
6
Main.java:
檔案:Main.java
public class Main
{
public static void main(String args[])
{
A a1 = new A() ;
B b1 ;
}
}
執行
java –verbose:class Main
之後,螢幕上的輸出如下:
這個輸出告訴我們,只有單獨宣告(如: B 類別)而已,是不會促使類別載入器幫
我們載入類別的,只有實體化指令(new XXX())才會讓類別載入器幫我們載入該
類別。
依需求載入的方式,可以讓執行時期所佔用的記憶體變小,這是因為我們的
Java 程式可能是由數以百計的類別所構成,但是不是每一種類別都會在執行的
時候使用,因此依需求載入的方式,可以讓需要的類別載入記憶體,而不需要的
類別不會被載入。這種機制這在記憶體不多的裝置上特別有用,因為Java 最初
就是為了嵌入式系統而設計,嵌入式裝置擁有的記憶體通常很小,所以Java 採
這種設計方式有其歷史因素。底下就是一個利用依需求載入功能來減少記憶體用
量的例子:
檔案:Office.java
public class Office
{
public static void main(String args[])
{
if(args[0].equals("Word"))
7
{
Word w = new Word() ;
w.print() ;
}else if(args[0].equals("Excel"))
{
Excel e = new Excel() ;
e.print() ;
}
}
}
檔案:Word.java
public class Word
{
public void print()
{
System.out.println("Using Word") ;
}
}
檔案:Excel.java
public class Excel
{
public void print()
{
System.out.println("Using Excel") ;
}
}
假設您有一個主程式叫 Office,而Office 又有兩個主要的應用程式Word
及Excel,兩個應用程式執行時都需要佔用很大的記憶體空間,而一般很少人同
時用到這兩個應用程式,這時利用上述Office.java 的程式寫法,就可以省去很
多記憶體。當我們執行指令:
java Office Word
時,程式就會載入Word.class,如下圖:
8
而執行指令:
java Office Excel
時,程式就會載入Excel.class,如下圖:
依需求載入的優點是節省記憶體,但是仍有其缺點。舉例來說,當程式第一
次用到該類別的時候,系統就必須花一些額外的時間來載入該類別,使得整體執
行效能受到影響,尤其是由數以萬計的類別所構成的Java 程式。可是往後需要
用到該類別時,由於類別在初次載入之後就會被永遠存放在記憶體之中,直到
Java 虛擬機器關閉,所以不再需要花費額外的時間來載入。
總的來說,就彈性上和速度上的考量,如此的設計所帶來的優點(彈性和省
記憶體)遠超過額外載入時間的花費(只有第一次用到時),因此依需求載入的設計
是明智的選擇。
█讓Java 程式具有動態性的兩種方法
Java 本質上具有的動態性,帶來了記憶體的節省、程式的彈性、以及一些額外
的載入時間。既然談到了動態性,『那麼,上述的程式是”有彈性”的寫法嗎? 』
一些寫程式的老手一定會這樣問。嚴格來說,上述的程式只達到了一點點的彈
性,就是讓使用者可以在執行的時候選擇載入哪個類別。接下來,筆者將示範讓
程式具有更多彈性的做法。
要讓程式具有彈性,就必須利用 Java 所提供的動態性來完成。Java 提供
兩種方法來達成動態性。一種是隱式的(implicit),另一種是顯式的(explicit)。
這兩種方式底層用到的機制完全相同,差異只有在程式設計師所使用的程式碼有
所不同,隱式的方法可以讓您在不知不覺的情況下就使用,而顯式的方法必須加
入一些額外的程式碼。您可以把這兩種方法和Win32 應用程式呼叫動態聯結函
9
式庫(Dynamic Linking Library)時的兩種方法(implicit 與explicit)來類比,意
思幾乎相同。
隱式的(implicit)方法我們已經談過了,也就是當程式設計師用到new 這個
Java 關鍵字時,會讓類別載入器依需求載入您所需要的類別,這種方式使用了
隱式的(implicit)方法,筆者在前面提到『一般使用Java 程式語言來開發應用程
式的工程師眼中,很少有機會能夠察覺Java 因為具備了動態性之後所帶來的優
點和特性,甚至根本不曾利用過這個Java 先天就具有的特性。這不是我們的
錯,而是因為這個動態的本質被巧妙地隱藏起來,使得使用Java 的程式設計師
在不知不覺中用到了動態性而不自知』,就是因為如此。但是,隱式的(implicit)
方法仍有其限制,無法達成更多的彈性,遇到這種情況,我們就必須動用顯式的
(explicit)方法來完成。
顯式的方法,又分成兩種方式,一種是藉由java.lang.Class 裡的forName()
方法,另一種則是藉由java.lang.ClassLoader 裡的loadClass()方法。您可以
任意選用其中一種方法。
隱式的
使用new關鍵字
Java所提供的動態性
Java提供的動態性
顯式的
使用Class的
forName方法
使用ClassLoader的
loadClass方法
█用顯式的方法來達成動態性:使用Class.forName()
方法
使用顯式的方法來達成動態性,意味著我們要自己動手處理類別載入時的細
節部分。處理細節部分雖然需要撰寫一些額外的程式碼,但是可以讓程式變的更
具彈性,我們以上面這個Office.java、Word.java、以及Excel.java 的例子來
說,這個程式雖然很有彈性(可以讓執行程式的人在執行時期決定要載入哪個類
10
別),但是,如果我們新增了Access.java 和PowerPoint.java 這兩個新類別時,
Office.java 裡的主程式就必須增加兩個if … else 的迴圈。身為一個強調程式
維護性的工程師,接下來要問的一定是:「那麼,有沒有更好的方法,可以在不
修改主程式的情況下增加主程式的功能?」有的,使用顯式的方法所達成的動態
性,可以增加程式的彈性,並達成我們不希望修改主程式的需求。程式碼如下所
示:
檔案:Assemblyjava
public interface Assembly
{
public void start() ;
}
檔案:Office.java
public class Office
{
public static void main(String args[]) throws Exception
{
Class c = Class.forName(args[0]) ;
Object o = c.newInstance() ;
Assembly a = (Assembly) o ;
a.start() ;
}
}
檔案:Word.java
public class Word implements Assembly
{
public void start()
{
System.out.println("Word starts") ;
}
}
檔案:Excel.java
public class Excel implements Assembly
{
public void start()
11
{
System.out.println("Excel starts") ;
}
}
如此一來,我們的主程式 Office.java 只要編譯之後,往後只要叫用:
java Office Word 或java Office Excel
就可以動態載入我們需要的類別,如下圖所示:
除此之外,輸入
java Office Access
的時候,雖然會出現錯誤訊息(因為我們還沒有完成Access.java),如下圖所示:
但 是 , 一 旦 往 後 我 們 完 成 了 Access(Access.java) 這個類別, 或是
PowerPoint(PowerPoint.java),只要他們都實作了Assembly 這個介面,我們
12
就可以在不修改主程式(Office.java)的情況下,新增主程式的功能:
檔案:Access.java
public class Access implements Assembly
{
public void start()
{
System.out.println("Access starts") ;
}
}
檔案:PowerPoint.java
public class PowerPoint implements Assembly
{
public void start()
{
System.out.println("PowerPoint starts") ;
}
}
如果您用過 JDBC 撰寫資料庫程式,使用Class.forName()來動態載入類別的
功能,正是JDBC 裡頭用來動態載入JDBC 驅動程式(JDBC Software driver)
的方式。
注 意 : 請仔細端看加入 –verbose:class 之後的螢幕輸出, 您會看到
Assembly.class 也被系統載入了。在此您可以發現,interface 如同class 一般,
會由編譯器產生一個獨立的類別檔(.class),當類別載入器載入類別時,如果發
現該類別繼承了其他類別,或是實作了其他介面,就會先載入代表該介面的類別
檔,也會載入其父類別的類別檔,如果父類別也有其父類別,也會一併優先載入。
換句話說,類別載入器會依繼承體系最上層的類別往下依序載入,直到所有的祖
先類別都載入了,才輪到自己載入。舉例來說,如果有個類別C 繼承了類別B、
實作了介面I,而B 類別又繼承自A 類別,那麼載入的順序如下圖:
如果您親自搜尋 Java 2 SDK 說明檔內部對於Class 這個類別的說明,您可
以發現其實有兩個forName()方法,一個是只有一個參數的(就是之前程式之中
所使用的):
public static Class forName(String className)
13
另外一個是需要三個參數的:
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 這個參數的用法,請看範例程式:
檔案:Office.java
public class Office
{
public static void main(String args[]) throws Exception
{
Class c = Class.forName(args[0],true,null) ;
Object o = c.newInstance() ;
Assembly a = (Assembly) o ;
a.start() ;
}
}
執行時輸入
java Office Word
螢幕上的輸出為:
之所以產生畫面上的錯誤訊息,是因為我們在類別載入器的部分給的參數是
null,導致系統不知道用哪個類別載入器來載入類別檔,因而發生錯誤。如果把
程式修改如下:
檔案:Office.java
public class Office
{
14
public static void main(String args[]) throws Exception
{
Office off = new Office() ;
Class c = Class.forName(args[0],true,off.getClass().getClassLoader()) ;
Object o = c.newInstance() ;
Assembly a = (Assembly) o ;
a.start() ;
}
}
輸出就正常了:
從這裡我們可以知道,forName()方法的第三個參數,是用來指定載入類別的類
別載入器的, 只有一個參數的forName() 方法, 由於在內部使用了
ClassLoader.getCallerClassLoader()來取得載入呼叫他的類別所使用的類別
載入器, 和我們自己寫的程式有相同的效用。( 注意,
ClassLoader.getCallerClassLoader()是一個private 的方法,所以我們無法
自行叫用,因此必須要自己產生一個Office 類別的實體,再去取得載入Office
類別時所使用的類別載入器)。
那麼 forName()的第二個參數的效用為何? 我們修改主程式如下:
檔案:Office.java
public class Office
{
public static void main(String args[]) throws Exception
{
Office off = new Office() ;
System.out.println("類別準備載入") ;
Class c = Class.forName(args[0],true,off.getClass().getClassLoader()) ;
System.out.println("類別準備實體化") ;
Object o = c.newInstance() ;
Object o2 = c.newInstance() ;
}
}
而Word.java 之中則加入靜態初始化區塊:
15
檔案:Word.java
public class Word implements Assembly
{
static
{
System.out.println("Word static initialization") ;
}
public void start()
{
System.out.println("Word starts") ;
}
}
執行:
java Office Word
時的結果如下:
如果 forName()方法的第二個參數給的是false:
檔案:Office.java
public class Office
{
public static void main(String args[]) throws Exception
{
Office off = new Office() ;
System.out.println("類別準備載入") ;
Class c = Class.forName(args[0],false,off.getClass().getClassLoader()) ;
System.out.println("類別準備實體化") ;
Object o = c.newInstance() ;
Object o2 = c.newInstance() ;
}
}
則輸出變成:
16
使用 true 和false 會造成不同的輸出結果,您看出端倪了嗎? 過去在很多
Java 的書本上提到靜態初始化區塊(static initialization block)時,都會說「靜
態初始化區塊是在類別第一次載入的時候才會被呼叫那僅僅一次。」可是從上
面的輸出,卻發現即使類別被載入了,其靜態初始化區塊也沒有被呼叫,而是在
第一次叫用newInstance()方法時,靜態初始化區塊才真正被叫用。所以嚴格
來說,應該改成「靜態初始化區塊是在類別第一次被實體化的時候才會被呼叫那
僅僅一次。」
所以,我們得到以下結論:不管您使用的是new 來產生某類別的實體、或是使用
只有一個參數的forName()方法,內部都隱含了”載入類別+呼叫靜態初始化區
塊”的動作。而使用具有三個參數的forName()方法時,如果第二個參數給定的
是false,那麼就只會命令類別載入器載入該類別,但不會叫用其靜態初始化區
塊,只有等到整個程式第一次實體化某個類別時,靜態初始化區塊才會被叫用。
█用顯式的方法來達成動態性:直接使用類別載入器
要直接使用類別載入器幫我們載入類別,首先必須先取得指向類別載入器的
參考才行,而要取得指向類別載入器的參考之前,必須先取得某物件所屬的類別。
相信在大家的認知裡,都有個基本的概念,就是:「類別是一個樣版,而物
件就是根據這個樣版所產生出來的實體」。在Java 之中,每個類別最後的老祖
宗都是Object,而Object 裡有一個名為getClass()的方法,就是用來取得某
特定實體所屬類別的參考, 這個參考, 指向的是一個名為Class 類別
(Class.class) 的實體,您無法自行產生一個Class 類別的實體,因為它的建構
式被宣告成private,這個Class 類別的實體是在類別檔(.class)第一次載入記憶
體時就建立的,往後您在程式中產生任何該類別的實體,這些實體的內部都會有
一個欄位記錄著這個Class 類別的所在位置。概念上如下圖所示:
17
類別實體與Class.class
Class.class的實體
為檔案系統中某X.class
在記憶體中的代理人
X.class的實體
Class.class的實體
為檔案系統中某Y.class
在記憶體中的代理人
X.class的實體
Y.class的實體
基本上,我們可以把每個Class 類別的實體,當作是某個類別在記憶體中的代理
人。每次我們需要查詢該類別的資料(如其中的field、method 等)時,就可以
請這個實體幫我們代勞。事實上,Java 的Reflection 機制,就大量地利用Class
類別。去深入Class 類別的原始碼,我們可以發現Class 類別的定義中大多數的
方法都是原生方法(native method)。
在 Java 之中,每個類別都是由某個類別載入器(ClassLoader 的實體)來載
入,因此,Class 類別的實體中,都會有欄位記錄著載入它的ClassLoader 的
實體(注意:如果該欄位是null,並不代表它不是由類別載入器所載入,而是代表
這個類別由靴帶式載入器(bootstrap loader,也有人稱root loader)所載入,只
不過因為這個載入器並不是用Java 所寫成,所以邏輯上沒有實體 )。其概念如
下圖所示:
18
類別實體,Class.class,與ClassLoader
Class.class的實體
(X.class)
X.class的實體
Class.class的實體
(Y.class)
X.class的實體
Y.class的實體
Z.class的實體
Z.class的實體
Z.class的實體Class.class的實體
(Z.class)
ClassLoader
實體
ClassLoader
實體
從上圖我們可以得之,系統裡同時存在多個 ClassLoader 的實體,而且一個類
別載入器不限於只能載入一個類別,類別載入器可以載入多個類別。所以,只要
取得Class 類別實體的參考,就可以利用其getClassLoader()方法籃取得載入
該類別之類別載入器的參考。getClassLoader()方法最後會呼叫原生方法
getClassLoader0(),其宣告如下:
private native ClassLoader getClassLoader0();
最後,取得了ClassLoader 的實體,我們就可以叫用其loadClass()方法幫我們
載入我們想要的類別。因此,我們把程式碼修改如下:
檔案:Office.java
public class Office
{
public static void main(String args[]) throws Exception
{
Office off = new Office() ;
System.out.println("類別準備載入") ;
ClassLoader loader = off.getClass().getClassLoader() ;
Class c = loader.loadClass(args[0]) ;
System.out.println("類別準備實體化") ;
Object o = c.newInstance() ;
Object o2 = c.newInstance() ;
}
}
執行
19
java Office Word
之後,結果如下:
從這裡我們也可以看出,直接使用ClassLoader 類別的loadClass()方法來載入
類別,只會把類別載入記憶體,並不會叫用該類別的靜態初始化區塊,而必須等
到第一次實體化該類別時,該類別的靜態初始化區塊才會被叫用。這種情形與使
用Class 類別的forName()方法時,第二個參數傳入false 幾乎是相同的結果。
上述的程式其實還有另外一種寫法如下所示:
檔案:Office.java
public class Office
{
public static void main(String args[]) throws Exception
{
Class cb = Office.class ;
System.out.println("類別準備載入") ;
ClassLoader loader = cb.getClassLoader() ;
Class c = loader.loadClass(args[0]) ;
System.out.println("類別準備實體化") ;
Object o = c.newInstance() ;
Object o2 = c.newInstance() ;
}
}
直接在程式裡頭使用Office.class,就是直接取得某特定實體所屬類別的參考,
用起來比起產生Office 的實體,再用getClass()取出,這個方法方便的多,也
較省記憶體。
█自己建立類別載入器來載入類別
在此之前,當我們談到使用類別載入器來載入類別時,都是使用既有的類別
載入器來幫我們載入我們所指定的類別。那麼,我們可以自己產生類別載入器來
幫我們載入類別嗎? 答案是肯定的。利用Java 本身提供的
java.net.URLClassLoader 類別就可以做到,範例如下:
檔案:Office.java
import java.net.* ;
20
public class Office
{
public static void main(String args[]) throws Exception
{
URL u = new URL("file:/d:/my/lib/") ;
URLClassLoader ucl = new URLClassLoader(new URL[]{ u }) ;
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 arga[])方法的那個類別)的相對目錄,我們可以在URL
的地方只寫”file:lib/”,代表相對於目前的目錄。
我們再把程式修改如下:
檔案:Office.java
import java.net.* ;
public class Office
{
public static void main(String args[]) throws Exception
{
URL u = new URL("file:/d:/my/lib/") ;
URLClassLoader ucl = new URLClassLoader(new URL[]{ u }) ;
Class c = ucl.loadClass(args[0]) ;
Assembly asm = (Assembly) c.newInstance() ;
asm.start() ;
URL u1 = new URL("file:/d:/my/lib/") ;
URLClassLoader ucl1 = new URLClassLoader(new URL[]{ u1 }) ;
Class c1 = ucl1.loadClass(args[0]) ;
Assembly asm1 = (Assembly) c1.newInstance() ;
asm1.start() ;
21
}
}
您將發現螢幕輸出如下:
您可以發現,同樣一個類別,卻被不同的 URLClassLoader 分別載入,而且分
別初始化一次。也就是說,在一個虛擬機器之中,相同的類別被載入了兩次。
注意!
此範例中,我們的目錄結構如下圖:
█類別被哪個類別載入器載入?
我們將上述的程式碼稍作修改,修改後的程式碼如下:
檔案:Office.java
import java.net.* ;
public class Office
{
public static void main(String args[]) throws Exception
22
{
URL u = new URL("file:/d:/my/lib/") ;
URLClassLoader ucl = new URLClassLoader(new URL[]{ u }) ;
Class c = ucl.loadClass(args[0]) ;
Assembly asm = (Assembly) c.newInstance() ;
asm.start() ;
URL u1 = new URL("file:/d:/my/lib/") ;
URLClassLoader ucl1 = new URLClassLoader(new URL[]{ u1 }) ;
Class c1 = ucl1.loadClass(args[0]) ;
Assembly asm1 = (Assembly) c1.newInstance() ;
asm1.start() ;
System.out.println(Office.class.getClassLoader()) ;
System.out.println(u.getClass().getClassLoader()) ;
System.out.println(ucl.getClass().getClassLoader()) ;
System.out.println(c.getClassLoader()) ;
System.out.println(asm.getClass().getClassLoader()) ;
System.out.println(u1.getClass().getClassLoader()) ;
System.out.println(ucl1.getClass().getClassLoader()) ;
System.out.println(c1.getClassLoader()) ;
System.out.println(asm1.getClass().getClassLoader()) ;
}
}
執行後輸出結果如下圖:
從輸出中我們可以得知,Office.class 由AppClassLoader(又稱做System
23
Loader,系統載入器)所載入,URL.class 與URLClassLoader.class 由Bootstrap
Loader 所載入(注意:輸出null 並非代表不是由類別載入器所載入。在Java 之
中,所有的類別都必須由類別載入器載入才行,只不過Bootstrap Loader 並非
由Java 所撰寫而成,而是由C++實作而成,因此以Java 的觀點來看,邏輯上
並沒有Bootstrap Loader 的類別實體)。而Word.class 分別由兩個不同的
URLClassLoader 實體載入。至於Assembly.class , 本身應該是由
AppClassLoader 載入,但是由於多型(Polymorphism)的關係,所指向的類別
實體(Word.class)由特定的載入器所載入,導致列印在螢幕上的內容是其所參考
的類別實體之類別載入器。Interface 這種型態本身無法直接使用new 來產生
實體,所以在執行getClassLoader()的時候,叫用的一定是所參考的類別實體
的getClassLoader(),要知道Interface 本身由哪個類別載入器載入,您必須
使用底下程式碼:
Assembly.class.getClassLoader()
如果把這行程式加在上述程式之中,您會發現其輸出結果為
這個輸出告訴您,Assembly.class 是由AppClassLoader 載入。
█一切都是由Bootstrap Loader 開始 : 類別載入器
的階層體系
注意!
如果您參考其他資料,您將發現在講解類別載入器時,資料中常常會提到「類別
載入器的階層體系」(classloader hierarchy)。請注意,這裡所說的繼承體系,
並非我們一般所指的類別階層體系(即父類別與子類別構成的體系)。「類別載入
器的階層體系」的意涵另有所指,請不要混淆了。筆者接下來將為您解釋「類別
載入器的階層體系」所代表的真正意涵。
在前面我們曾經提過,Java 程式在編譯之後會產生許多的執行單位(.class
檔),當我們執行主要類別時(有public static void main(String arga[])方法的
那個類別),才由虛擬機器一一載入所有需要的執行單位,變成一個邏輯上為一
體的Java 應用程式。因此接下來,我們將細部討論這整個流程。
當我們在命令列輸入 java xxx.class 的時候,java.exe 根據我們之前所提
過的邏輯找到了JRE(Java Runtime Environment),接著找到位在JRE 之中的
jvm.dll(真正的Java 虛擬機器),最後載入這個動態聯結函式庫,啟動Java 虛
擬機器。這個動作的詳細介紹請回頭參閱第一章。
虛擬機器一啟動,會先做一些初始化的動作,比方說抓取系統參數等。一旦
初始化動作完成之後,就會產生第一個類別載入器,即所謂的Bootstrap
Loader,Bootstrap Loader 是由C++所撰寫而成(所以前面我們說,以Java
24
的觀點來看,邏輯上並不存在Bootstrap Loader 的類別實體,所以在Java 程
式碼裡試圖印出其內容的時候,我們會看到的輸出為null),這個Bootstrap
Loader 所做的初始工作中,除了也做一些基本的初始化動作之外,最重要的就
是載入定義在sun.misc 命名空間底下的Launcher.java 之中的
ExtClassLoader( 因為是inner class , 所以編譯之後會變成
Launcher$ExtClassLoader.class),並設定其Parent 為null,代表其父載入器
為Bootstrap Loader。然後Bootstrap Loader 再要求載入定義於sun.misc 命
名空間底下的Launcher.java 之中的AppClassLoader(因為是inner class,
所以編譯之後會變成Launcher$AppClassLoader.class),並設定其Parent
為之前產生的ExtClassLoader 實體。這裡要請大家注意的是,
Launcher$ExtClassLoader.class 與Launcher$AppClassLoader.class 都是
由Bootstrap Loader 所載入,所以Parent 和由哪個類別載入器載入沒有關係。
我們可以用下圖來表示:
Bootstrap Loader
(c++) Extended Loader
(Java)
Parent
載入
System Loader
(Java)
載入Parent
AppClassLoader 在Sun 官方文件中常常又被稱做系統載入器(System
Loader),但是在本文中為了避免混淆,所以還是稱作AppClassLoader。最後
一個步驟,是由AppClassLoader 負責載入我們在命令列之中所輸入的
xxx.class(注意:實際上xxx.class 很可能由ExtClassLoader 或Bootstrap
Loader 載入,請參考底下「委派模型」一節),然後開始一個Java 應用程式的
生命週期。上述整個流程如下圖所示:
25
找到JRE
命令列:java xxx.class
找到jvm.dll
載入
ExtClassLoader
產生
Bootstrp Loader
啟動JVM
並進行初始化
載入
AppClassLoader
載入
這個由 Bootstrap Loader

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics