熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

關於Java程序中類加載完全揭密

2022-06-13   來源: Java核心技術 

  類加載是java語言提供的最強大的機制之一盡管類加載並不是討論的熱點話題但所有的編程人員都應該了解其工作機制明白如何做才能讓其滿足我們的需要這能有效節省我們的編碼時間從不斷調試ClassNotFoundException ClassCastException的工作中解脫出來

這篇文章從基礎講起比如代碼與數據的不同之處是什麼他們是如何構成一個實例或對象的然後深入探討java虛擬機(JVM)是如何利用類加載器讀取代碼以及java中類加載器的主要類型接著用一個類加載的基本算法看一下類加載器如何加載一個內部類本文的下一節演示一段代碼來說明擴展和開發屬於自己的類加載器的必要性緊接著解釋如何使用定制的類加載器來完成一個一般意義上的任務使其可以加載任意遠端客戶的代碼在JVM中定義實例化並執行它本文包括了JEE關於類加載的規范——事實上這已經成為了JEE的標准之一

類與數據

一個類代表要執行的代碼而數據則表示其相關狀態狀態時常改變而代碼則不會當我們將一個特定的狀態與一個類相對應起來也就意味著將一個類事例化盡管相同的類對應的實例其狀態千差萬別但其本質都對應著同一段代碼在JAVA中一個類通常有著一個class文件但也有例外在JAVA的運行時環境中(Java runtime)每一個類都有一個以第一類(firstclass)的Java對象所表現出現的代碼其是javalangClass的實例我們編譯一個JAVA文件編譯器都會嵌入一個public static final修飾的類型為javalangClass名稱為class的域變量在其字節碼文件中因為使用了public修飾我們可以采用如下的形式對其訪問

  javalangClass klass = Myclassclass;

  一旦一個類被載入JVM中同一個類就不會被再次載入了(切記同一個類)這裡存在一個問題就是什麼是同一個類?正如一個對象有一個具體的狀態即標識一個對象始終和其代碼(類)相關聯同理載入JVM的類也有一個具體的標識我們接下來看

在JAVA中一個類用其完全匹配類名(fully qualified class name)作為標識這裡指的完全匹配類名包括包名和類名但在JVM中一個類用其全名和一個加載類ClassLoader的實例作為唯一標識因此如果一個名為Pg的包中有一個名為Cl的類被類加載器KlassLoader的一個實例kl加載Cl的實例即Cclass在JVM中表示為(Cl Pg kl)這意味著兩個類加載器的實例(Cl Pg kl) 和 (Cl Pg kl)是不同的被它們所加載的類也因此完全不同互不兼容的那麼在JVM中到底有多少種類加載器的實例?下一節我們揭示答案

  類加載器

  在JVM中每一個類都被javalangClassLoader的一些實例來加載類ClassLoader是在包中javalang裡開發者可以自由地繼承它並添加自己的功能來加載類

無論何時我們鍵入java MyMainClass來開始運行一個新的JVM引導類加載器(bootstrap class loader)負責將一些關鍵的Java類如javalangObject和其他一些運行時代碼先加載進內存中運行時的類在JRE\lib\rtjar包文件中因為這屬於系統底層執行動作我們無法在JAVA文檔中找到引導類加載器的工作細節基於同樣的原因引導類加載器的行為在各JVM之間也是大相徑庭
同理如果我們按照如下方式

  log(javalangStringclassgetClassLoader());

來獲取java的核心運行時類的加載器就會得到null

接下來介紹java的擴展類加載器擴展庫提供比java運行代碼更多的特性我們可以把擴展庫保存在由javaextdirs屬性提供的路徑中

(編輯注javaextdirs屬性指的是系統屬性下的一個key所有的系統屬性可以通過SystemgetProperties()方法獲得在編者的系統中javaextdirs的value是 C:\Program Files\Java\jdk_\jre\lib\ext下面將要談到的如javaclasspath也同屬系統屬性的一個key)

類ExtClassLoader專門用來加載所有javaextdirs下的jar文件開發者可以通過把自己的jar文件或庫文件加入到擴展目錄的classpath使其可以被擴展類加載器讀取

從開發者的角度第三種同樣也是最重要的一種類加載器是AppClassLoader這種類加載器用來讀取所有的對應在javaclasspath系統屬性的路徑下的類

Sun的java指南中文章理解擴展類加載(Understanding Extension Class Loading)對以上三個類加載器路徑有更詳盡的解釋這是其他幾個JDK中的類加載器
URLClassLoader
●javasecuritySecureClassLoader
●javarmiserverRMIClassLoader
●sunappletAppletClassLoader

javalangThread包含了public ClassLoader getContextClassLoader()方法這一方法返回針對一具體線程的上下文環境類加載器此類加載器由線程的創建者提供以供此線程中運行的代碼在需要加載類或資源時使用如果此加載器未被建立缺省是其父線程的上下文類加載器原始的類加載器一般由讀取應用程序的類加載器建立

類加載器如何工作?
除了引導類加載器所有的類加載器都有一個父類加載器不僅如此所有的類加載器也都是javalangClassLoader類型以上兩種類加載器是不同的而且對於開發者自訂制的類加載器的正常運行也至關重要最重要的方面是正確設置父類加載器任何類加載器其父類加載器是加載該類加載器的類加載器實例(記住類加載器本身也是一個類!)

使用loadClass()方法可以從類加載器中獲得該類我們可以通過javalangClassLoader的源代碼來了解該方法工作的細節如下

  protected synchronized Class<?> loadClass
    (String name boolean resolve)
    throws ClassNotFoundException{

    // First check if the class is already loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parentloadClass(name false);
            } else {
                c = findBootstrapClass(name);
            }
        } catch (ClassNotFoundException e) {
            // If still not found then invoke
            // findClass to find the class
            c = findClass(name);
        }
    }
    if (resolve) {
            resolveClass(c);
    }
    return c;
}



我們可以使用ClassLoader的兩種構造方法來設置父類加載器

  public class MyClassLoader extends ClassLoader{

    public MyClassLoader(){
        super(MyClassLoaderclassgetClassLoader());
    }
}





  public class MyClassLoader extends ClassLoader{

    public MyClassLoader(){
        super(getClass()getClassLoader());
    }
}



第一種方式較為常用因為通常不建議在構造方法裡調用getClass()方法因為對象的初始化只是在構造方法的出口處才完全完成因此如果父類加載器被正確建立當要示從一個類加載器的實例獲得一個類時如果它不能找到這個類它應該首先去訪問其父類如果父類不能找到它(即其父類也不能找不這個類等等)而且如果findBootstrapClass()方法也失敗了則調用findClass()方法findClass()方法的缺省實現會拋出ClassNotFoundException當它們繼承javalangClassLoader來訂制類加載器時開發者需要實現這個方法findClass()的缺省實現方式如下

  protected Class<?> findClass(String name)
        throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }



在findClass()方法內部類加載器需要獲取任意來源的字節碼來源可以是文件系統URL數據庫可以產生字節碼的另一個應用程序及其他類似的可以產生java規范的字節碼的來源你甚至可以使用BCEL (Byte Code Engineering Library字節碼工程庫)它提供了運行時創建類的捷徑BCEL已經被成功地使用在以下方面編譯器優化器混淆器代碼產生器及其他分析工具一旦字節碼被檢索此方法就會調用defineClass()方法此行為對不同的類加載實例是有差異的因此如果兩個類加載實例從同一個來源定義一個類所定義的結果是不同的

JAVA語言規范(Java language specification)詳細解釋了JAVA執行引擎中的類或接口的加載(loading)鏈接(linking)或初始化(initialization)過程

圖一顯示了一個主類稱為MyMainClass的應用程序依照之前的闡述MyMainClassclass會被AppClassLoader加載 MyMainClass創建了兩個類加載器的實例CustomClassLoader 和 CustomClassLoader他們可以從某數據源(比如網絡)獲取名為Target的字節碼這表示類Target的類定義不在應用程序類路徑或擴展類路徑在這種情況下如果MyMainClass想要用自定義的類加載器加載Target類CustomClassLoader和CustomClassLoader會分別獨立地加載並定義Targetclass類這在java中有重要的意義如果Target類有一些靜態的初始化代碼並且假設我們只希望這些代碼在JVM中只執行一次而這些代碼在我們目前的步驟中會執行兩次——分別被不同的CustomClassLoaders加載並執行如果類Target被兩個CustomClassLoaders加載並創建兩個實例Target和Target如圖一顯示它們不是類型兼容的換句話說在JVM中無法執行以下代碼

  Target target = (Target) target;



以上代碼會拋出一個ClassCastException這是因為JVM把他們視為分別不同的類因為他們被不同的類加載器所定義這種情況當我們不是使用兩個不同的類加載器CustomClassLoader 和 CustomClassLoader而是使用同一個類加載器CustomClassLoader的不同實例時也會出現同樣的錯誤這些會在本文後邊用具體代碼說明

image
在同一個JVM中多個類加載器加載同一個目標類

關於類加載定義和鏈接的更多解釋請參考Andreas Schaefer的Inside Class Loaders

為什麼我們需要我們自己的類加載器
原因之一為開發者寫自己的類加載器來控制JVM中的類加載行為java中的類靠其包名和類名來標識對於實現了javaioSerializable接口的類serialVersionUID扮演了一個標識類版本的重要角色這個唯一標識是一個類名接口名成員方法及屬性等組成的一個位的哈希字段而且也沒有其他快捷的方式來標識一個類的版本嚴格說來如果以上的都匹配那麼則屬於同一個類

但是讓我們思考如下情況我們需要開發一個通用的執行引擎可以執行實現某一特定接口的任何任務當任務被提交到這個引擎首先需要加載這個任務的代碼假設不同的客戶對此引擎提交了不同的任務湊巧這些所有的任務都有一個相同的類名和包名現在面臨的問題就是這個引擎是否可以針對不同的用戶所提交的信息而做出不同的反應這一情況在下文的參考一節有可供下載的代碼樣例samepath 和 differentversions這兩個目錄分別演示了這一概念

顯示了文件目錄結構有三個子目錄samepath differentversions 和 differentversionspush裡邊是例子

image
文件夾結構組織示例

在samepath 中類versionVersion保存在v和v兩個子目錄裡兩個類具有同樣的類名和包名唯一不同的是下邊這行

  public void fx(){
        log(this = + this + ; Versionfx());
    }



V日志記錄中有Versionfx()而在v中則是Versionfx()把這個兩個存在細微不同的類放在一個classpath下然後運行Test類

set CLASSPATH=;%CURRENT_ROOT%\v;%CURRENT_ROOT%\v
%JAVA_HOME%\bin\java Test

顯示了控制台輸出我們可以看到對應著Versionfx()的代碼被執行了因為類加載器在classpath首先看到此版本的代碼

image
在類路徑中samepath測試排在最前面的version

再次運行類路徑做如下微小改動

set CLASSPATH=;%CURRENT_ROOT%\v;%CURRENT_ROOT%\v
%JAVA_HOME%\bin\java Test

控制台的輸出變為圖對應著Versionfx()的代碼被加載因為類加載器在classpath中首先找到它的路徑

image
在類路徑中samepath測試排在最前面的version

根據以上例子可以很明顯地看出類加載器加載在類路徑中被首先找到的元素如果我們在v和v中刪除了versionVersion做一個非versionVersion形式的jar文件如myextensionjar把它放到對應javaextdirs的路徑下再次執行後看到versionVersion不再被AppClassLoader加載而是被擴展類加載器加載如圖所示

image
AppClassLoader及ExtClassLoader

繼續這個例子文件夾differentversions包含了一個RMI執行引擎客戶端可以提供給執行引擎任何實現了commonTaskIntf接口的任務子文件夾client 和 client包含了類clientTaskImpl有個細微不同的兩個版本兩個類的區別在以下幾行

  static{
        log(clientTaskImplclassgetClassLoader
        (v) : + TaskImplclassgetClassLoader());
    }

    public void execute(){
        log(this = + this + ; execute());
    }



在client和client裡分別有getClassLoader(v) 與 execute()和getClassLoader(v) 與 execute()的的log語句並且在開始執行引擎RMI服務器的代碼中我們隨意地將client的任務實現放在類路徑的前面

CLASSPATH=%CURRENT_ROOT%\common;%CURRENT_ROOT%\server;
    %CURRENT_ROOT%\client;%CURRENT_ROOT%\client
%JAVA_HOME%\bin\java serverServer

如圖的屏幕截圖在客戶端VM各自的clientTaskImpl類被加載實例化並發送到服務端的VM來執行從服務端的控制台可以明顯看到clientTaskImpl代碼只被服務端的VM執行一次這個單一的代碼版本在服務端多次生成了許多實例並執行任務


image
執行引擎服務器控制台

顯示了服務端的控制台加載並執行兩個不同的客戶端的請求如圖78所示需要注意的是代碼只被加載了一次(從靜態初始化塊的日志中也可以明顯看出)但對於客戶端的調用這個方法被執行了兩次

image
圖7 執行引擎客戶端 控制台 

圖7中客戶端VM加載了含有clientTaskImplclassgetClassLoader(v)的日志內容的類TaskImpl的代碼並提供給服務端的執行引擎的客戶端VM加載了另一個TaskImpl的代碼並發送給服務端

image
執行引擎客戶端 控制台 

在客戶端的VM中類clientTaskImpl被分別加載初始化並發送到服務端執行還揭示了clientTaskImpl的代碼只在服務端的VM中加載了一次但這唯一的一次卻在服務端創造了許多實例並執行或許客戶端該不高興了因為並不是它的clientTaskImpl(v)的方法調用被服務端執行了而是其他的一些代碼如何解決這一問題?答案就是實現定制的類加載器

定制類加載器

要較好地控制類的加載就要實現定制的類加載器所有自定義的類加載器都應繼承自javalangClassLoader而且在構造方法中我們也應該設置父類加載器然後重寫findClass()方法differentversionspush文件夾包含了一個叫做FileSystemClassLoader的自訂制的類加載器其結構如圖所示

image
定制類加載器關系

以下是在commonFileSystemClassLoader實現的主方法

    

  public byte[] findClassBytes(String className){

        try{
            String pathName = currentRoot +
                FileseparatorChar + className
                replace( FileseparatorChar)
                + class;
            FileInputStream inFile = new
                FileInputStream(pathName);
            byte[] classBytes = new
                byte[inFileavailable()];
            inFileread(classBytes);
            return classBytes;
        }
        catch (javaioIOException ioEx){
            return null;
        }
    }

    public Class findClass(String name)throws
        ClassNotFoundException{

        byte[] classBytes = findClassBytes(name);
        if (classBytes==null){
            throw new ClassNotFoundException();
        }
        else{
            return defineClass(name classBytes
                 classByteslength);
        }
    }

    public Class findClass(String name byte[]
        classBytes)throws ClassNotFoundException{

        if (classBytes==null){
            throw new ClassNotFoundException(
                (classBytes==null));
        }
        else{
            return defineClass(name classBytes
                 classByteslength);
        }
    }

    public void execute(String codeName
        byte[] code){

        Class klass = null;
        try{
            klass = findClass(codeName code);
            TaskIntf task = (TaskIntf)
                klassnewInstance();
            taskexecute();
        }
        catch(Exception exception){
            exceptionprintStackTrace();
        }
    }



這個類供客戶端把clientTaskImpl(v)轉換成字節數組之後此字節數組被發送到RMI服務端在服務端一個同樣的類用來把字節數組的內容轉換回代碼客戶端代碼如下

  public class Client{

    public static void main (String[] args){

        try{
            byte[] code = getClassDefinition
                (clientTaskImpl);
            serverIntfexecute(clientTaskImpl
                code);
            }
            catch(RemoteException remoteException){
                remoteExceptionprintStackTrace();
            }
        }

    private static byte[] getClassDefinition
        (String codeName){
        String userDir = SystemgetProperties()
            getProperty(BytePath);
        FileSystemClassLoader fscl = null;

        try{
            fscl = new FileSystemClassLoader
                (userDir);
        }
        catch(FileNotFoundException
            fileNotFoundException){
            fileNotFoundExceptionprintStackTrace();
        }
        return fsclfindClassBytes(codeName);
    }
}



在執行引擎中從客戶端收到的代碼被送到定制的類加載器中定制的類加載器把其從字節數組定義成類實例化並執行需要指出的是對每一個客戶請求我們用類FileSystemClassLoader的不同實例來定義客戶端提交的clientTaskImpl而且clientTaskImpl並不在服務端的類路徑中這也就意味著當我們在FileSystemClassLoader調用findClass()方法時findClass()調用內在的defineClass()方法類clientTaskImpl被特定的類加載器實例所定義因此當FileSystemClassLoader的一個新的實例被使用類又被重新定義為字節數組因此對每個客戶端請求類clientTaskImpl被多次定義我們就可以在相同執行引擎JVM中執行不同的clientTaskImpl的代碼

    

  public void execute(String codeName byte[] code)throws RemoteException{

        FileSystemClassLoader fileSystemClassLoader = null;

        try{
            fileSystemClassLoader = new FileSystemClassLoader();
            fileSystemClassLoaderexecute(codeName code);
        }
        catch(Exception exception){
            throw new RemoteException(exceptiongetMessage());
        }
    }

  示例在differentversionspush文件夾下服務端和客戶端的控制台界面分別如圖所示

  image

  圖 定制類加載器執行引擎

顯示的是定制的類加載器控制台我們可以看到clientTaskImpl的代碼被多次加載實際上針對每一個客戶端類都被加載並初始化

image
    圖 定制類加載器客戶端

    圖含有clientTaskImplclassgetClassLoader(v)的日志記錄的類TaskImpl的代碼被客戶端的VM加載然後送到服務端 另一個客戶端把包含有clientTaskImplclassgetClassLoader(v)的類代碼加載並送往服務端

image
定制類加載器客戶端

這段代碼演示了我們如何利用不同的類加載器實例來在同一個VM上執行不同版本的代碼

JEE的類加載器

  JEE的服務器傾向於以一定間隔頻率丟棄原有的類並重新載入新的類在某些情況下會這樣執行而有些情況則不同樣對於一個web服務器如果要丟棄一個servlet實例可能是服務器管理員的手動操作也可能是此實例長時間未相應當一個JSP頁面被首次請求容器會把此JSP頁面翻譯成一個具有特定形式的servlet代碼一旦servlet代碼被創建容器就會把這個servlet翻譯成class文件等待被使用對於提交給容器的每次請求容器都會首先檢查這個JSP文件是否剛被修改過是的話就重新翻譯此文件這可以確保每次的請求都是及時更新的企業級的部署方案以ear war rar等形式的文件同樣需要重復加載可能是隨意的也可能是依照某種配置方案定期執行對所有的這些情況——類的加載卸載重新加載……全部都是建立在我們控制應用服務器的類加載機制的基礎上的實現這些需要擴展的類加載器它可以執行由其自身所定義的類Brett Peterson已經在他的文章 Understanding JEE Application Server Class Loading Architectures給出了JEE應用服務器的類加載方案的詳細說明詳見網站

結要

本文探討了類載入到虛擬機是如何進行唯一標識的以及類如果存在同樣的類名和包名時所產生的問題因為沒有一個直接可用的類版本管理機制所以如果我們要按自己的意願來加載類時需要自己訂制類加載器來擴展其行為我們可以利用許多JEE服務器所提供的熱部署功能來重新加載一個新版本的類而不改動服務器的VM即使不涉及應用服務器我們也可以利用定制類加載器來控制java應用程序載入類時的具體行為Ted Neward的書ServerBased Java Programming中詳細闡述java的類加載JEE的API以及使用他們的最佳途徑


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26923.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.