Java 類加載機制分析

在編寫 Java 程序時,我們所編寫的 .java 文件經編譯后,生成能被 JVM 識別的 .class 文件,.class 文件以字節碼格式存儲類或接口的結構描述數據。JVM 將這些數據加載至內存指定區域后,依此來構造類實例。

1. 類加載過程

JVM 將來自 .class 文件或其他途徑的類字節碼數據加載至內存,并對數據進行驗證、解析、初始化,使其最終轉化為能夠被 JVM 使用的 Class 對象,這個過程稱為 JVM 的類加載機制。

2. ClassLoader

ClassLoader 是 Java 中的類加載器,負責將 Class 加載到 JVM 中,不同的 ClassLoader 具有不同的等級,這將在稍后解釋。

2.1 ClassLoader的作用

ClassLoader 的作用有以下 3點:

  • 將 Class 字節碼解析轉換成 JVM 所要求的 java.lang.Class 對象
  • 判斷 Class 應該由何種等級的 ClassLoader 負責加載
  • 加載 Class 到 JVM中

2.2 ClassLoader的主要方法

ClassLoader 中包含以下幾個主要方法:

  • defineClass

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    

    作用:將 byte 字節流轉換為 java.lang.Class 對象。
    說明:字節流可以來源于.class文件,也可來自網絡或其他途徑。調用 defineClass 方法時,會對字節流進行校驗,校驗不通過會拋出 ClassFormatError 異常。該方法返回的 Class 對象還沒有 resolve(鏈接),可以顯示調用 resolveClass 方法對 Class 進行 resolve,或者在 Class 真正實例化時,由 JVM 自動執行 resolve.

  • resolveClass

    protected final void resolveClass(Class<?> c)
    

    作用 :對 Class 進行鏈接,把單一的 Class 加入到有繼承關系的類樹中。

  • findClass

    Class<?> findClass(String name)
    

    作用:根據類的 binary name,查找對應的 java.lang.Class 對象。
    說明:binary name 是類的全名,如 String 類的 binary name 為 java.lang.String。findClass 通常和 defineClass 一起使用,下面將舉例說明二者關系。
    舉例:java.net.URLClassLoader 是 ClassLoader 的子類,它重寫了 ClassLoader中的 findClass 和 defineClass 方法,我們看下 findClass 的主方法體。

    // 入參為 Class 的 binary name,如 java.lang.String
    protected Class<?> findClass(final String name) throws ClassNotFoundException {
        // 以上代碼省略
      
        // 通過 binary name 生成包路徑,如 java.lang.String -> java/lang/String.class
        String path = name.replace('.', '/').concat(".class");
        // 根據包路徑,找到該 Class 的文件資源
        Resource res = ucp.getResource(path, false);
        if (res != null) {
            try {
               // 調用 defineClass 生成 java.lang.Class 對象
                return defineClass(name, res);
            } catch (IOException e) {
                throw new ClassNotFoundException(name, e);
            }
        } else {
            return null;
        }
      
        // 以下代碼省略
    }
    
  • loadClass

     public Class<?> loadClass(String name)
    

    作用:加載 binary name 對應的類,返回 java.lang.Class 對象
    說明:loadClass 和 findClass 都是接受類的 binary name 作為入參,返回對應的 Class 對象,但是二者在內部實現上卻是不同的。loadClass 方法實現了 ClassLoader 的等級加載機制。我們看下 loadClass 方法的具體實現:

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
      synchronized (getClassLoadingLock(name)) {
             // First, check if the class has already been loaded
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      c = parent.loadClass(name, false);
                  } else {
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }
    
              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // to find the class.
                  long t1 = System.nanoTime();
                  c = findClass(name);
    
                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {
              resolveClass(c);
          }
          return c;
      }
    }
    

    loadClass 方法的實現流程主要為:

    1. 調用 findLoadedClass 方法檢查目標類是否被加載過,如果未加載過,則進行下面的加載步驟
    2. 如果存在父加載器,則調用父加載器的loadClass 方法加載類
    3. 父加載類不存在時,調用 JVM 內部的 ClassLoader 加載類
    4. 經過 2,3 步驟,若還未成功加載類,則使用該 ClassLoader 自身的 findClass 方法加載類
    5. 最后根據入參 resolve 判斷是否需要 resolveClass,返回 Class 對象

    loadClas 默認是同步方法,在實現自定義 ClassLoader 時,通常的做法是繼承 ClassLoader,重寫 findClass 方法而非 loadClass 方法。這樣既能保留類加載過程的等級加載機制和線程安全性,又可實現從不同數據來源加載類。

3. ClassLoader 的等級加載機制

上文已經提到 Java 中存在不同等級的 ClassLoader,且類加載過程中運用了等級加載機制,下面將進行詳細解釋。

3.1 Java 中的四層 ClassLoader

  • Bootstrap ClassLoader
    又稱啟動類加載器。Bootstrap ClassLoader 是 Java 中最頂層的 ClassLoader,它負責加載 JDK 中的核心類庫,如 rt.jar,charset.jar,這些是 JVM 自身工作所需要的類。

    Bootstarp ClassLoader 由 JVM 控制,我們無法訪問到這個類。雖然它位于類記載器的頂層,但它沒有子加載器。需要通過 native 方法,來調用 Bootstap ClassLoader 來加載類,如下:

    private native Class<?> findBootstrapClass(String name);
    

    以下代碼能夠輸出 Bootstrap ClassLoader 加載的類庫路徑:

    System.out.print(System.getProperty("sun.boot.class.path"));
    
    運行結果:
    C:\Software\Java8\jre\lib\resources.jar;
    C:\Software\Java8\jre\lib\rt.jar;
    C:\Software\Java8\jre\lib\jsse.jar;
    C:\Software\Java8\jre\lib\jce.jar;
    C:\Software\Java8\jre\lib\charsets.jar;
    C:\Software\Java8\jre\lib\jfr.jar;
    C:\Software\Java8\src.zip
    
  • Ext ClassLoader

    又稱擴展類加載器。Ext ClassLoader 負責加載 JDK 中的擴展類庫,這些類庫位于 /JAVA_HOME/jre/lib/ext/ 目錄下。如果我們將自己編寫的類打包丟到該目錄下,則該類將由 Ext ClassLoader 負責加載。

    以下代碼能夠輸出 Ext ClassLoader 加載的類庫路徑:

    System.out.println(System.getProperty("java.ext.dirs"));
    
    運行結果:
    C:\Software\Java8\jre\lib\ext;
    C:\Windows\Sun\Java\lib\ext
    

    這里自定義了一個類加載器,全名為 com.eric.learning.java._classloader.FileClassLoader,我們想讓它能夠由 Ext ClassLoader加載,需要進行如下步驟:

    • 在 /JAVA_HOME/jre/lib/ext/ 目錄下按照類的包結構新建目錄
    • 將編譯好的 FileClassLoader.class 丟到目錄 /JAVA_HOME/jre/lib/ext/com/eric/learning/java/_classloader 下
    • 運行命令 jar cf test.jar com,生成 test.jar
    • 現在就可以用 ExtClassLoader 來加載類 FileClassLoader 了
      ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();
      Class<?> clazz = classLoader.loadClass("com.eric.learning.java._classloader.FileClassLoader");
      System.out.println(clazz.getName());
      

      ClassLoader.getSystemClassLoader() 獲得的是 Ext ClassLoader 的子加載器, App ClassLoader

  • App ClassLoader

    繼承關系圖

    又稱系統類加載器,App ClassLoader 負責加載項目 classpath 下的 jar 和 .class 文件,我們自己編寫的類一般有它負責加載。App ClassLoader 的父加載器為 Ext ClassLoader。

    以下代碼能夠輸出 App ClassLoader 加載的 .class 和 jar 文件路徑:

     System.out.println(System.getProperty("java.class.path"));
    
    運行結果:
    C:\Coding\learning\target\classes;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.8.8\jackson-core-2.8.8.jar;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.8.8\jackson-databind-2.8.8.jar;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.8.8\jackson-annotations-2.8.8.jar
    

    筆者的項目通過 Maven 來管理,\target\class 是 Maven 工程里 .class 文件的默認存儲路徑,其余如 jackson-core-2.8.8.jar 是通過 Maven 引入的第三方依賴包。

  • Custom ClassLoader
    自定義類加載器,自定義類加載器需要繼承抽象類 ClassLoader 或它的子類,并且所有 Custom ClassLoader 的父加載器都是 AppClassLoader,下面簡單解釋下這點。

    抽象類 ClassLoader 中有2種形式的構造方法:

    // 1
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    // 2
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    

    構造器1 以 getSystemClassLoader() 作為父加載器,而這個方法返回的即是 AppClassLoader。
    構造器2 表面上看允許我們指定當前類加載器的parent,但是如果我們試圖將 Custom ClassLoader 的構造方法寫成如下形式:

    public class FileClassLoader extends ClassLoader {
        public FileClassLoader(ClassLoader parent) {
            super(parent);
        }
    }
    

    在構造 FileClassLoader 實例時,new FileClassLoader( ClassLoader ) 將拋出異常:

    Java 的 security manager 不允許自定義類構造器訪問上述的 ClassLoader 的構造方法。

3.2 等級加載機制

? 如同我們在抽象類 ClassLoader 的 loadClass 方法所看到那樣,當通過一個 ClassLoader 加載類時,會先自底向上檢查父加載器是否已加載過該類,如果加載過則直接返回 java.lang.Class 對象。如果一直到頂層的 BootstrapClassLoader 都未加載過該類,則又會自頂向下嘗試加載。如果所有層級的 ClassLoader 都未成功加載類,最終將拋出 ClassNotFoundException。如下圖所示:


3.3 為何采用等級加載機制

? 首先,采用等級加載機制,能夠防止同一個類被重復加載,如果父加載器已經加載過某個類,再次加載時會直接返回 java.lang.Class 對象。

? 其次,不同等級的類加載器的存在能保證類加載過程的安全性。如果只存在一個等級的 ClassLoader,那么我們可以用自定義的 String 類替換掉核心類庫中的 String 類,這會造成安全隱患。而現在由于在 JVM 啟動時就會加載 String 類,所以即便存在相同 binary name 的 String 類,它也不會再被加載。

4. 從 JVM 角度看類加載過程

? 在 JVM 加載類時,會將讀取 .class 文件中的類字節碼數據,并解析拆分成 JVM 能識別的幾個部分,這些不同的部分都將被存儲在 JVM 的 方法區。然后 JVM 會在 堆區 創建一個 java.lang.Class 對象,用來封裝該類在方法區的數據。 如下圖所示:

? 上文提到 .class 文件中的類字節碼數據,會被 JVM 拆分成不同部分存儲在方法區,而方法區實際就是用于存儲類結構信息的地方。我們看看方法區都有哪些東西:

-   類及其父類的 binary name
-   類的類型 (class or interface)
-   訪問修飾符 (public,abstract,final 等)
-   實現的接口的全名列表
-   常量池
-   字段信息
-   方法信息
-   靜態變量
-   ClassLoader 引用
-   Class 引用

? 方法區存儲的這些類的各部分結構信息,能通過 java.lang.Class 類中的不同方法獲得,可以說 Class 對象是對類結構數據的封裝。

5. 一個簡單的自定義類加載器例子

// 傳入 .class 文件的絕對路徑,加載 Class
public class FileClassLoader extends ClassLoader {

    // 重寫了 findClass 方法
    @Override
    public Class<?> findClass(String path) throws ClassNotFoundException {
        File file = new File(path);
        if (!file.exists()) {
            throw new ClassNotFoundException();
        }
        
        byte[] classBytes = getClassData(file);
        if (classBytes == null || classBytes.length == 0) {
            throw new ClassNotFoundException();
        }
        return defineClass(classBytes, 0, classBytes.length);
    }

    private byte[] getClassData(File file) {
        try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new  
             ByteArrayOutputStream()) {
            byte[] buffer = new byte[4096];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[] {};
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容