這篇文章從基礎講起,好比代碼和數據的不一樣之處是什麼,他們是怎麼構成一個實例或對象的。而後深刻探討java虛擬機(JVM)是怎麼利用類加載器讀取代碼,及java中類加載器的主要類型。接着用一個類加載的基本算法看一下類加載器怎麼加載一個內部類。本文的下一節演示一段代碼來講明擴展和研發屬於本身的類加載器的必要性。緊接着解釋怎麼使用制定的類加載器來完成一個通常意義上的任務,使其能加載任意遠端客戶的代碼,在JVM中定義,實例化並執行他。本文包括了J2EE關於類加載的規範??事實上這已成爲了J2EE的標準之一。 java
類和數據 web
一個類表明要執行的代碼,而數據則表示其相關狀態。狀態時常改動,而代碼則不會。當咱們將一個特定的狀態和一個類相對應起來,也就意味着將一個類事例化。儘管相同的類對應的實例其狀態千差萬別,但其本質都對應着同一段代碼。在JAVA中,一個類一般有着一個.class文件,但也有例外。在JAVA的運行時環境中(Java runtime),每個類都有一個以第一類(first-class)的Java對象所表現出現的代碼,其是java.lang.Class的實例。咱們編譯一個JAVA文件,編譯器都會嵌入一個public, static, final修飾的類型爲java.lang.Class,名稱爲class的域變量在其字節碼文件中。由於使用了public修飾,咱們能採用以下的形式對其訪問: 算法
java.lang.Class klass = Myclass.class; 數據庫
一旦一個類被載入JVM中,同一個類就不會被再次載入了(切記,同一個類)。這裏存在一個問題就是什麼是「同一個類」?正如一個對象有一個具體的狀態,即標識,一個對象始終和其代碼(類)相關聯。同理,載入JVM的類也有一個具體的標識,咱們接下來看。 編程
在JAVA中,一個類用其徹底匹配類名(fully qualified class name)做爲標識,這裏指的徹底匹配類名包括包名和類名。但在JVM中一個類用其全名和一個加載類ClassLoader的實例做爲惟一標識。所以,若是一個名爲Pg的包中,有一個名爲Cl的類,被類加載器KlassLoader的一個實例kl1加載,Cl的實例,即C1.class在JVM中表示爲(Cl, Pg, kl1)。這意味着兩個類加載器的實例(Cl, Pg, kl1) 和 (Cl, Pg, kl2)是不一樣的,被他們所加載的類也所以徹底不一樣,互不兼容的。那麼在JVM中到底有多少種類加載器的實例?下一節咱們揭示答案。 bootstrap
類加載器 數組
在JVM中,每個類都被java.lang.ClassLoader的一些實例來加載.類ClassLoader是在包中java.lang裏,研發者能自由地繼承他並添加本身的功能來加載類。 服務器
不管什麼時候咱們鍵入java MyMainClass來開始運行一個新的JVM,「引導類加載器(bootstrap class loader)」負責將一些關鍵的Java類,如java.lang.Object和其餘一些運行時代碼先加載進內存中。運行時的類在JRElibrt.jar包文件中。由於這屬於系統底層執行動做,咱們沒法在JAVA文件中找到引導類加載器的工做細節。基於一樣的緣由,引導類加載器的行爲在各JVM之間也是截然不同。 網絡
同理,若是咱們按照以下方式: 併發
log(java.lang.String.class.getClassLoader());
來獲取java的核心運行時類的加載器,就會獲得null。
接下來介紹java的擴展類加載器。擴展庫提供比java運行代碼更多的特性,咱們能把擴展庫保存在由java.ext.dirs屬性提供的路徑中。
(編輯注:java.ext.dirs屬性指的是系統屬性下的一個key,全部的系統屬性能經過System.getProperties()方法得到。在編者的系統中,java.ext.dirs的value是」 C:Program FilesJavajdk1.5.0_04jrelibext」。下面將要談到的如java.class.path也同屬系統屬性的一個key。)
類ExtClassLoader專門用來加載全部java.ext.dirs下的.jar文件。研發者能經過把本身的.jar文件或庫文件加入到擴展目錄的classpath,使其能被擴展類加載器讀取。
從研發者的角度,第三種一樣也是最重要的一種類加載器是AppClassLoader。這種類加載器用來讀取全部的對應在java.class.path系統屬性的路徑下的類。
Sun的java指南中,文章「理解擴展類加載」(Understanding Extension Class Loading)對以上三個類加載器路徑有更詳盡的解釋,這是其餘幾個JDK中的類加載器
●java.net.URLClassLoader
●java.security.SecureClassLoader
●java.rmi.server.RMIClassLoader
●sun.applet.AppletClassLoader
java.lang.Thread,包含了public ClassLoader getContextClassLoader()方法,這一方法返回針對一具體線程的上下文環境類加載器。此類加載器由線程的建立者提供,以供此線程中運行的代碼在須要加載類或資源時使用。若是此加載器未被創建,缺省是其父線程的上下文類加載器。原始的類加載器通常由讀取應用程式的類加載器創建。
類加載器怎麼工做?
除了引導類加載器,全部的類加載器都有一個父類加載器,不只如此,全部的類加載器也都是java.lang.ClassLoader類型。以上兩種類加載器是不一樣的,並且對於研發者自訂製的類加載器的正常運行也相當重要。最重要的方面是正確設置父類加載器。全部類加載器,其父類加載器是加載該類加載器的類加載器實例。(記住,類加載器自己也是個類!)
使用loadClass()方法能從類加載器中得到該類。咱們能經過java.lang.ClassLoader的原始碼來了解該方法工做的細節,以下:
咱們能使用ClassLoader的兩種構造方法來設置父類加載器:
或
第一種方式較爲經常使用,由於一般不建議在構造方法裏調用getClass()方法,由於對象的初始化只是在構造方法的出口處才徹底完成。所以,若是父類加載器被正確創建,當要示從一個類加載器的實例得到一個類時,若是他不能找到這個類,他應該首先去訪問其父類。若是父類不能找到他(即其父類也不能找不這個類,等等),並且若是findBootstrapClass0()方法也失敗了,則調用findClass()方法。findClass()方法的缺省實現會拋出ClassNotFoundException,當他們繼承java.lang.ClassLoader來訂製類加載器時研發者須要實現這個方法。findClass()的缺省實現方式以下:
在findClass()方法內部,類加載器須要獲取任意來源的字節碼。來源能是文件系統,URL,數據庫,能產生字節碼的另外一個應用程式,及其餘相似的能產生java規範的字節碼的來源。你甚至能使用BCEL (Byte Code Engineering Library:字節碼工程庫),他提供了運行時建立類的捷徑。BCEL已被成功地使用在如下方面:編譯器,優化器,混淆器,代碼產生器及其餘分析工具。一旦字節碼被檢索,此方法就會調用defineClass()方法,此行爲對不一樣的類加載實例是有差別的。所以,若是兩個類加載實例從同一個來源定義一個類,所定義的結果是不一樣的。
JAVA語言規範(Java language specification)周詳解釋了JAVA執行引擎中的類或接口的加載(loading),連接(linking)或初始化(initialization)過程。
圖一顯示了一個主類稱爲MyMainClass的應用程式。依照以前的闡述,MyMainClass.class會被AppClassLoader加載。 MyMainClass建立了兩個類加載器的實例:CustomClassLoader1 和 CustomClassLoader2,他們能從某數據源(好比網絡)獲取名爲Target的字節碼。這表示類Target的類定義不在應用程式類路徑或擴展類路徑。在這種狀況下,若是MyMainClass想要用自定義的類加載器加載Target類,CustomClassLoader1和CustomClassLoader2會分別獨立地加載並定義Target.class類。這在java中有重要的意義。若是Target類有一些靜態的初始化代碼,而且假設咱們只但願這些代碼在JVM中只執行一次,而這些代碼在咱們目前的步驟中會執行兩次??分別被不一樣的CustomClassLoaders加載並執行。若是類Target被兩個CustomClassLoaders加載並建立兩個實例Target1和Target2,如圖一顯示,他們不是類型兼容的。換句話說,在JVM中沒法執行如下代碼:
Target target3 = (Target) target2;
以上代碼會拋出一個ClassCastException。這是由於JVM把他們視爲分別不一樣的類,由於他們被不一樣的類加載器所定義。這種狀況當咱們不是使用兩個不一樣的類加載器CustomClassLoader1 和 CustomClassLoader2,而是使用同一個類加載器CustomClassLoader的不一樣實例時,也會出現一樣的錯誤。這些會在本文後邊用具體代碼說明。
圖1. 在同一個JVM中多個類加載器加載同一個目標類
關於類加載、定義和連接的更多解釋,請參考Andreas Schaefer的"Inside Class Loaders."
爲何咱們須要咱們本身的類加載器
緣由之一爲研發者寫本身的類加載器來控制JVM中的類加載行爲,java中的類靠其包名和類名來標識,對於實現了java.io.Serializable接口的類,serialVersionUID扮演了一個標識類版本的重要角色。這個惟一標識是個類名、接口名、成員方法及屬性等組成的一個64位的哈希字段,並且也沒有其餘快捷的方式來標識一個類的版本。嚴格說來,若是以上的都匹配,那麼則屬於同一個類。
不過讓咱們思考以下狀況:咱們須要研發一個通用的執行引擎。能執行實現某一特定接口的全部任務。當任務被提交到這個引擎,首先須要加載這個任務的代碼。假設不一樣的客戶對此引擎提交了不一樣的任務,湊巧,這些全部的任務都有一個相同的類名和包名。目前面臨的問題就是這個引擎是否能針對不一樣的用戶所提交的信息而作出不一樣的反應。這一狀況在下文的參考一節有可供下載的代碼樣例,samepath 和 differentversions,這兩個目錄分別演示了這一律念。
圖2 顯示了文件目錄結構,有三個子目錄samepath, differentversions, 和 differentversionspush,裏邊是例子:
圖2. 目錄結構組織示例
在samepath 中,類version.Version保存在v1和v2兩個子目錄裏,兩個類具備一樣的類名和包名,惟一不一樣的是下邊這行:
V1中,日誌記錄中有Version.fx(1),而在v2中則是Version.fx(2)。把這個兩個存在細微不一樣的類放在一個classpath下,而後運行Test類:
set CLASSPATH=.;%CURRENT_ROOT%v1;%CURRENT_ROOT%v2
%JAVA_HOME%binjava Test
圖3顯示了控制檯輸出。咱們能看到對應着Version.fx(1)的代碼被執行了,由於類加載器在classpath首先看到此版本的代碼。
圖3. 在類路徑中samepath測試排在最前面的version 1
再次運行,類路徑作以下微小改動。
set CLASSPATH=.;%CURRENT_ROOT%v2;%CURRENT_ROOT%v1
%JAVA_HOME%binjava Test
控制檯的輸出變爲圖4。對應着Version.fx(2)的代碼被加載,由於類加載器在classpath中首先找到他的路徑。
圖4. 在類路徑中samepath測試排在最前面的version 2
根據以上例子能很是明顯地看出,類加載器加載在類路徑中被首先找到的元素。若是咱們在v1和v2中刪除了version.Version,作一個非version.Version形式的.jar文件,如myextension.jar,把他放到對應java.ext.dirs的路徑下,再次執行後看到version.Version再也不被AppClassLoader加載,而是被擴展類加載器加載。如圖5所示。
圖5. AppClassLoader及ExtClassLoader
繼續這個例子,目錄differentversions包含了一個RMI執行引擎,客戶端能提供給執行引擎全部實現了common.TaskIntf接口的任務。子目錄client1 和 client2包含了類client.TaskImpl有個細微不一樣的兩個版本。兩個類的差異在如下幾行:
在client1和client2裏分別有getClassLoader(v1) 和 execute(1)和getClassLoader(v2) 和 execute(2)的的log語句。而且,在開始執行引擎RMI服務器的代碼中,咱們隨意地將client2的任務實現放在類路徑的前面。
CLASSPATH=%CURRENT_ROOT%common;%CURRENT_ROOT%server;
%CURRENT_ROOT%client2;%CURRENT_ROOT%client1
%JAVA_HOME%binjava server.Server
如圖6,7,8的屏幕截圖,在客戶端VM,各自的client.TaskImpl類被加載、實例化,併發送到服務端的VM來執行。從服務端的控制檯,能明顯看到client.TaskImpl代碼只被服務端的VM執行一次,這個單一的代碼版本在服務端屢次生成了許多實例,並執行任務。
圖6. 執行引擎服務器控制檯
圖6顯示了服務端的控制檯,加載並執行兩個不一樣的客戶端的請求,如圖7,8所示。須要注意的是,代碼只被加載了一次(從靜態初始化塊的日誌中也能明顯看出),但對於客戶端的調用這個方法被執行了兩次。
圖7. 執行引擎客戶端 1控制檯
圖7中,客戶端VM加載了含有client.TaskImpl.class.getClassLoader(v1)的日誌內容的類TaskImpl的代碼,並提供給服務端的執行引擎。圖8的客戶端VM加載了另外一個TaskImpl的代碼,併發送給服務端。
圖8. 執行引擎客戶端 2控制檯
在客戶端的VM中,類client.TaskImpl被分別加載,初始化,併發送到服務端執行。圖6還揭示了client.TaskImpl的代碼只在服務端的VM中加載了一次,但這「惟一的一次」卻在服務端創造了許多實例並執行。或許客戶端1該不高興了由於並非他的client.TaskImpl(v1)的方法調用被服務端執行了,而是其餘的一些代碼。怎麼解決這一問題?答案就是實現制定的類加載器。
制定類加載器
要較好地控制類的加載,就要實現制定的類加載器。全部自定義的類加載器都應繼承自java.lang.ClassLoader。並且在構造方法中,咱們也應該設置父類加載器。而後重寫findClass()方法。differentversionspush目錄包含了一個叫作FileSystemClassLoader的自訂製的類加載器。其結構如圖9所示。
圖9. 制定類加載器關係
如下是在common.FileSystemClassLoader實現的主方法:
這個類供客戶端把client.TaskImpl(v1)轉換成字節數組,以後此字節數組被髮送到RMI服務端。在服務端,一個一樣的類用來把字節數組的內容轉換回代碼。客戶端代碼以下:
在執行引擎中,從客戶端收到的代碼被送到制定的類加載器中。制定的類加載器把其從字節數組定義成類,實例化並執行。須要指出的是,對每個客戶請求,咱們用類FileSystemClassLoader的不一樣實例來定義客戶端提交的client.TaskImpl。並且,client.TaskImpl並不在服務端的類路徑中。這也就意味着當咱們在FileSystemClassLoader調用findClass()方法時,findClass()調用內在的defineClass()方法。類client.TaskImpl被特定的類加載器實例所定義。所以,當FileSystemClassLoader的一個新的實例被使用,類又被從新定義爲字節數組。所以,對每一個客戶端請求類client.TaskImpl被屢次定義,咱們就能在相同執行引擎JVM中執行不一樣的client.TaskImpl的代碼。
示例在differentversionspush目錄下。服務端和客戶端的控制檯界面分別如圖10,11,12所示:
圖10. 制定類加載器執行引擎
圖10顯示的是制定的類加載器控制檯。咱們能看到client.TaskImpl的代碼被屢次加載。實際上針對每個客戶端,類都被加載並初始化。
圖11. 制定類加載器,客戶端1
圖11中,含有client.TaskImpl.class.getClassLoader(v1)的日誌記錄的類TaskImpl的代碼被客戶端的VM加載,而後送到服務端。圖12 另外一個客戶端把包含有client.TaskImpl.class.getClassLoader(v1)的類代碼加載並送往服務端。
圖12. 制定類加載器,客戶端1
這段代碼演示了咱們怎麼利用不一樣的類加載器實例來在同一個VM上執行不一樣版本的代碼。
J2EE的類加載器
J2EE的服務器傾向於以必定間隔頻率,丟棄原有的類並從新載入新的類。在某些狀況下會這樣執行,而有些狀況則不。一樣,對於一個web服務器若是要丟棄一個servlet實例,多是服務器管理員的手動操做,也多是此實例長時間未相應。當一個JSP頁面被首次請求,容器會把此JSP頁面翻譯成一個具備特定形式的servlet代碼。一旦servlet代碼被建立,容器就會把這個servlet翻譯成class文件等待被使用。對於提交給容器的每次請求,容器都會首先檢查這個JSP文件是否剛被修改過。是的話就從新翻譯此文件,這能確保每次的請求都是及時更新的。企業級的部署方案以.ear, .war, .rar等形式的文件,一樣須要重複加載,多是隨意的也多是依照某種設置方案按期執行。對全部的這些狀況??類的加載、卸載、從新加載……所有都是創建在咱們控制應用服務器的類加載機制的基礎上的。實現這些須要擴展的類加載器,他能執行由其自身所定義的類。Brett Peterson已在他的文章 Understanding J2EE Application Server Class Loading Architectures給出了J2EE應用服務器的類加載方案的周詳說明,詳見網站TheServerSide.com。
結要
本文探討了類載入到虛擬機是怎麼進行惟一標識的,及類若是存在一樣的類名和包名時所產生的問題。由於沒有一個直接可用的類版本管理機制,因此若是咱們要按本身的意願來加載類時,須要本身訂製類加載器來擴展其行爲。咱們能利用許多J2EE服務器所提供的「熱部署」功能來從新加載一個新版本的類,而不改動服務器的VM。即便不涉及應用服務器,咱們也能利用制定類加載器來控制java應用程式載入類時的具體行爲。Ted Neward的書Server-Based Java Programming中周詳闡述java的類加載,J2EE的API及使用他們的最佳途徑。