歡迎您光臨本站 註冊首頁

淺談JAVA 類加載器

←手機掃碼閱讀     f2h0b53ohn @ 2020-06-23 , reply:0

類加載機制

類加載器負責加載所有的類,系統為所有被載入內存中的類生成一個 java.lang.Class 實例。一旦一個類被載入 JVM 中,同個類就不會被再次載入了。現在的問題是,怎麼樣才算“同一個類”?

正如一個對象有一個唯一的標識一樣,一個載入 JVM 中的類也有一個唯一的標識。在 Java 中,一個類用其全限定類名(包括包名和類名)作為標識:但在 JVM 中,一個類用其全限定類名和其類加載器作為唯一標識。例如,如果在 pg 的包中有一個名為 Person 的類,被類加載器 ClassLoader 的實例 k1 負責加載,則該 Person 類對應的 Class 對象在 JVM 中表示為(Person、pg、k1)。這意味著兩個類加載器加載的同名類:(Person、pg、k1)和(Person、pg、k12)是不同的,它們所加載的類也是完全不同、互不兼容的。

當 JVM 啟動時,會形成由三個類加載器組成的初始類加載器層次結構。

  • Bootstrap ClassLoader:根類加載器。

  • Extension ClassLoader:擴展類加載器。

  • System ClassLoader:系統類加載器。

Bootstrap ClassLoader 被稱為引導(也稱為原始或根)類加載器,它負責加載 Java 的核心類。在Sun 的 JVM 中,當執行 java.exe 命令時,使用 -Xbootclasspath 或 -D 選項指定 sun.boot.class.path 系統屬性值可以指定加載附加的類。

JVM的類加載機制主要有如下三種。

  • 全盤負責。所謂全盤負責,就是當一個類加載器負責加載某個 Class 時,該 Class 所依賴的和引用的其他 Class 也將由該類加載器負責載入,除非顯式使用另外一個類加載器來載入。

  • 父類委託。所謂父類委託,則是先讓 parent(父)類加載器試圖加載該 Class,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。

  • 緩存機制。緩存機制將會保證所有加載過的 Class 都會被緩存,當程序中需要使用某個 Class 時,類加載器先從緩存區中搜尋該 Class,只有當緩存區中不存在該 Class 對象時,系統才會讀取該類對應的二進制數據,並將其轉換成 Class 對象,存入緩存區中。這就是為什麼修改了 Class 後,必須重新啟動 JVM,程序所做的修改才會生效的原因。

除了可以使用 Java 提供的類加載器之外,開發者也可以實現自己的類加載器,自定義的類加載器通過繼承 ClassLoader 來實現。JVM 中這4種類加載器的層次結構如下圖所示。

注意:類加載器之間的父子關係並不是類繼承上的父子關係,這裡的父子關係是類加載器實例之間的關係

下面程序示範了訪問 JVM 的類加載器。

  public class ClassLoaderPropTest {   public static void main(String[] args) throws IOException {   // 獲取系統類加載器   ClassLoader systemLoader = ClassLoader.getSystemClassLoader();   System.out.println("系統類加載器:" + systemLoader);   /*    * 獲取系統類加載器的加載路徑――通常由CLASSPATH環境變量指定 如果操作系統沒有指定CLASSPATH環境變量,默認以當前路徑作為    * 系統類加載器的加載路徑    */   Enumerationem1 = systemLoader.getResources("");   while (em1.hasMoreElements()) {    System.out.println(em1.nextElement());   }   // 獲取系統類加載器的父類加載器:得到擴展類加載器   ClassLoader extensionLader = systemLoader.getParent();   System.out.println("擴展類加載器:" + extensionLader);   System.out.println("擴展類加載器的加載路徑:" + System.getProperty("java.ext.dirs"));   System.out.println("擴展類加載器的parent: " + extensionLader.getParent());   }  }

 

運行上面的程序,會看到如下運行結果

系統類加載器:sun.misc.Launcher$AppClassLoader@73d16e93
 file:/F:/EclipseProjects/demo/bin/
 擴展類加載器:sun.misc.Launcher$ExtClassLoader@15db9742
 擴展類加載器的加載路徑:C:Program FilesJavajre1.8.0_181libext;C:WindowsSunJavalibext
 擴展類加載器的parent: null

從上面運行結果可以看出,系統類加載器的加載路徑是程序運行的當前路徑,擴展類加載器的加載路徑是null(與 Java8 有區別),但此處看到擴展類加載器的父加載器是null,並不是根類加載器。這是因為根類加載器並沒有繼承 ClassLoader 抽象類,所以擴展類加載器的 getParent() 方法返回null。但實際上,擴展類加載器的父類加載器是根類加載器,只是根類加載器並不是 Java 實現的。

從運行結果可以看出,系統類加載器是 AppClassLoader 的實例,擴展類加載器 ExtClassLoader 的實例。實際上,這兩個類都是 URLClassLoader 類的實例。

注意:JVM 的根類加載器並不是 Java 實現的,而且由於程序通常無須訪問根類加載器,因此訪問擴展類加載器的父類加載器時返回null。

類加載器加載 Class 大致要經過如下8個步驟。

  1. 檢測此 Class 是否載入過(即在緩存區中是否有此Class),如果有則直接進入第8步,否則接著執行第2步。

  2. 如果父類加載器不存在(如果沒有父類加載器,則要麼 parent 一定是根類加載器,要麼本身就是根類加載器),則跳到第4步執行;如果父類加載器存在,則接著執行第3步。

  3. 請求使用父類加載器去載入目標類,如果成功載入則跳到第8步,否則接著執行第5步。

  4. 請求使用根類加載器來載入目標類,如果成功載入則跳到第8步,否則跳到第7步。

  5. 當前類加載器嘗試尋找 Class 文件(從與此 ClassLoader 相關的類路徑中尋找),如果找到則執行第6步,如果找不到則跳到第7步。

  6. 從文件中載入 Class,成功載入後跳到第8步。

  7. 拋出 ClassNotFoundExcepuon 異常。

  8. 返回對應的 java.lang.Class 對象。

其中,第5、6步允許重寫 ClassLoader的 findClass() 方法來實現自己的載入策略,甚至重寫 loadClass() 方法來實現自己的載入過程。

創建並使用自定義的類加載器
 

JVM 中除根類加載器之外的所有類加載器都是 ClassLoader 子類的實例,開發者可以通過擴展 ClassLoader 的子類,並重寫該 ClassLoader 所包含的方法來實現自定義的類加載器。查閱API文檔中關於 ClassLoader 的方法不難發現,ClassLoader 中包含了大量的 protected 方法――這些方法都可被子類重寫。

ClassLoader 類有如下兩個關鍵方法。

  • loadClass(String name, boolean resolve):該方法為 ClassLoader 的入口點,根據指定名稱來加載類,系統就是調用 ClassLoader 的該方法來獲取指定類對應的 Class 對象。

  • findClass(String name):根據指定名稱來查找類。

如果需要實現自定義的 ClassLoader,則可以通過重寫以上兩個方法來實現,通常推薦重寫 findClass() 方法,而不是重寫 loadClass() 方法。loadClass() 方法的執行步驟如下。

  1. 用 findLoadedClass(String) 來檢查是否已經加載類,如果已經加載則直接返回。

  2. 在父類加載器上調用 loadClass() 方法。如果父類加載器為null,則使用根類加載器來加載。

  3. 調用 findClass(String) 方法查找類。

從上面步驟中可以看出,重寫 findClass()方法可以避免覆蓋默認類加載器的父類委託、緩衝機制兩種策略:如果重寫 loadClass() 方法,則實現邏輯更為複雜。

在 ClassLoader 裡還有一個核心方法:Class defineClass(String name, byte[] b, int off,int len) 該方法負責將指定類的字節碼文件(即 Class 文件,如 Hello.class)讀入字節數組 byte[] b 內,並把它轉換為 Class對象,該字節碼文件可以來源於文件、網絡等。

defineClass() 方法管理 JVM 的許多複雜的實現,它負責將字節碼分析成運行時數據結構,並校驗有效性等。不過不用擔心,程序員無須重寫該方法。實際上該方法是 final 的,即使想重寫也沒有機會。

除此之外,ClassLoader 裡還包含如下一些普通方法。

  • findSystemClass(String name):從本地文件系統裝入文件。它在本地文件系統中尋找類文件,如果存在,就使用 defineClass() 方法將原始字節轉換成 Class 對象,以將該文件轉換成類。

  • static getSystemClassLoader():這是一個靜態方法,用於返回系統類加載器。

  • getParent():獲取該類加載器的父類加載器。

  • resolveClass(Class

    c):鏈接指定的類。類加載器可以使用此方法來鏈接類c。讀者無須理會關於此方法的太多細節。
  • findLoadedClass(String name):如果此 Java 虛擬機已加載了名為 name 的類,則直接返回該類對應的 Class 實例,否則返回null,該方法是 Java 類加載緩存機制的體現。

下面程序開發了一個自定義的 ClassLoader,該 ClassLoader 通過重寫 findClass() 方法來實現自定義的類加載機制。這個 ClassLoader 可以在加載類之前先編譯該類的文件,從而實現運行 Java 之前先編譯該程序的目標,這樣即可通過該 ClassLoader 直接運行 Java 源文件。

  public class CompileClassLoader extends ClassLoader {   // 讀取一個文件的內容   private byte[] getBytes(String filename) throws IOException {   File file = new File(filename);   long len = file.length();   byte[] raw = new byte[(int) len];   try (FileInputStream fin = new FileInputStream(file)) {    // 一次讀取class文件的全部二進制數據    int r = fin.read(raw);    if (r != len)    throw new IOException("無法讀取全部文件:" + r + " != " + len);    return raw;   }   }     // 定義編譯指定Java文件的方法   private boolean compile(String javaFile) throws IOException {   System.out.println("CompileClassLoader:正在編譯 " + javaFile + "...");   // 調用系統的javac命令   Process p = Runtime.getRuntime().exec("javac " + javaFile);   try {    // 其他線程都等待這個線程完成    p.waitFor();   } catch (InterruptedException ie) {    System.out.println(ie);   }   // 獲取javac線程的退出值   int ret = p.exitValue();   // 返回編譯是否成功   return ret == 0;   }     // 重寫ClassLoader的findClass方法   protected Class findClass(String name) throws ClassNotFoundException {   Class clazz = null;   // 將包路徑中的點(.)替換成斜線(/)。   String fileStub = name.replace(".", "/");   String javaFilename = fileStub + ".java";   String classFilename = fileStub + ".class";   File javaFile = new File(javaFilename);   File classFile = new File(classFilename);   // 當指定Java源文件存在,且class文件不存在、或者Java源文件   // 的修改時間比class文件修改時間更晚,重新編譯   if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) {    try {    // 如果編譯失敗,或者該Class文件不存在    if (!compile(javaFilename) || !classFile.exists()) {     throw new ClassNotFoundException("ClassNotFoundExcetpion:" + javaFilename);    }    } catch (IOException ex) {    ex.printStackTrace();    }   }   // 如果class文件存在,系統負責將該文件轉換成Class對象   if (classFile.exists()) {    try {    // 將class文件的二進制數據讀入數組    byte[] raw = getBytes(classFilename);    // 調用ClassLoader的defineClass方法將二進制數據轉換成Class對象    clazz = defineClass(name, raw, 0, raw.length);    } catch (IOException ie) {    ie.printStackTrace();    }   }   // 如果clazz為null,表明加載失敗,則拋出異常   if (clazz == null) {    throw new ClassNotFoundException(name);   }   return clazz;   }     // 定義一個主方法   public static void main(String[] args) throws Exception {   // 如果運行該程序時沒有參數,即沒有目標類   if (args.length < 1) {    System.out.println("缺少目標類,請按如下格式運行Java源文件:");    System.out.println("java CompileClassLoader ClassName");   }   // 第一個參數是需要運行的類   String progClass = args[0];   // 剩下的參數將作為運行目標類時的參數,   // 將這些參數複製到一個新數組中   String[] progArgs = new String[args.length - 1];   System.arraycopy(args, 1, progArgs, 0, progArgs.length);   CompileClassLoader ccl = new CompileClassLoader();   // 加載需要運行的類   Class clazz = ccl.loadClass(progClass);   // 獲取需要運行的類的主方法    Method main = clazz.getMethod("main", (new String[0]).getClass());   Object[] argsArray = { progArgs };   main.invoke(null, argsArray);   }  }

 

上面程序中的粗體字代碼重寫了 findClass() 方法,通過重寫該方法就可以實現自定義的類加載機制。在本類的 findClass() 方法中先檢查需要加載類的 Class 文件是否存在,如果不存在則先編譯源文件,再調用 ClassLoader 的 defineClass() 方法來加載這個 Class 文件,並生成相應的 Class 對象。

接下來可以隨意提供一個簡單的主類,該主類無須編譯就可以使用上面的 CompileClassLoader 來運行它。

  public class Hello {   public static void main(String[] args) {   for (String arg : args) {    System.out.println("運行Hello的參數:" + arg);   }   }  }

 

本示例程序提供的類加載器功能比較簡單,僅僅提供了在運行之前先編譯 Java 源文件的功能。實際上,使用自定義的類加載器,可以實現如下常見功能。

  • 執行代碼前自動驗證數字簽名。

  • 根據用戶提供的密碼解密代碼,從而可以實現代碼混淆器來避免反編譯 *.class 文件。

  • 根據用戶需求來動態地加載類。

  • 根據應用需求把其他數據以字節碼的形式加載到應用中。

URLClassLoader 類
 

Java 為 ClassLoader 提供了一個 URLClassLoader 實現類,該類也是系統類加載器和擴展類加載器的父類(此處的父類,就是指類與類之間的繼承關係)。URLClassLoader 功能比較強大,它既可以從本地文件系統獲取二進制文件來加載類,也可以從遠程主機獲取二進制文件來加載類。

在應用程序中可以直接使用 URLClassLoader 加載類,URLClassLoader 類提供瞭如下兩個構造器。

  • URLClassLoader(URL[] urls):使用默認的父類加載器創建一個 ClassLoader 對象,該對象將從 urls 所指定的系列路徑來查詢並加載類。

  • URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父類加載器創建一個 ClassLoader 對象,其他功能與前一個構造器相同。

一旦得到了 URLClassLoader 對象之後,就可以調用該對象的 loadClass() 方法來加載指定類。下面程序示範瞭如何直接從文件系統中加載 MySQL 驅動,並使用該驅動來獲取數據庫連接。通過這種方式來獲取數據厙連接,可以無須將 MySQL 驅動添加到 CLASSPATH 環境變量中。

  public class URLClassLoaderTest {   private static Connection conn;     // 定義一個獲取數據庫連接方法   public static Connection getConn(String url, String user, String pass) throws Exception {   if (conn == null) {    // 創建一個URL數組     URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") };    // 以默認的ClassLoader作為父ClassLoader,創建URLClassLoader    URLClassLoader myClassLoader = new URLClassLoader(urls);    // 加載MySQL的JDBC驅動,並創建默認實例    Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance();    // 創建一個設置JDBC連接屬性的Properties對象    Properties props = new Properties();    // 至少需要為該對象傳入user和password兩個屬性    props.setProperty("user", user);    props.setProperty("password", pass);    // 調用Driver對象的connect方法來取得數據庫連接    conn = driver.connect(url, props);   }   return conn;   }     public static void main(String[] args) throws Exception {   System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147"));   }  }

 

上面程序中的前兩行粗體字代碼創建了一個 URLClassLoader 對象,該對象使用默認的父類加載器,該類加載器的類加載路徑是當前路徑下的 mysql-connector-java-5.1.30-bin.jar 文件,將 MySQL 驅動複製到該路徑下,這樣保證該 ClassLoader 可以正常加載到 com.mysql.jdbc.Driver 類。

程序的第三行粗體字代碼使用 ClassLoader 的 loadClass() 加載指定類,並調用 Class 對象的 newInstance() 方法創建了一個該類的默認實例――也就是得到 com.mysql.jdbc.Driver 類的對象,當然該對象的實現類實現了 java.sql.Driver 接口,所以程序將其強制類型轉換為 Driver,程序的最後一行粗體字代碼通過 Driver 而不是 DriverManager 來獲取數據庫連接,關於 Driver 接口的用法讀者可以自行查閱API文檔。

正如前面所看到的,創建 URLClassLoader 時傳入了一個 URL 數組參數,該 ClassLoader 就可以從這系列 URL 指定的資源中加載指定類,這裡的 URL 可以以 file: 為前綴,表明從本地文件系統加載;可以以 http: 為前綴,表明從互聯網通過 HTTP 訪問來加載;也可以以 ftp: 為前綴,表明從互聯網通過 FTP訪問來加載......功能非常強大。

 


[f2h0b53ohn ] 淺談JAVA 類加載器已經有224次圍觀

http://coctec.com/docs/java/show-post-239465.html