Java 類加載機制分析

在編寫 Java 程序時碴里,我們所編寫的 .java 文件經(jīng)編譯后沈矿,生成能被 JVM 識別的 .class 文件,.class 文件以字節(jié)碼格式存儲類或接口的結(jié)構(gòu)描述數(shù)據(jù)并闲。JVM 將這些數(shù)據(jù)加載至內(nèi)存指定區(qū)域后细睡,依此來構(gòu)造類實例谷羞。

1. 類加載過程

JVM 將來自 .class 文件或其他途徑的類字節(jié)碼數(shù)據(jù)加載至內(nèi)存帝火,并對數(shù)據(jù)進行驗證、解析湃缎、初始化犀填,使其最終轉(zhuǎn)化為能夠被 JVM 使用的 Class 對象,這個過程稱為 JVM 的類加載機制嗓违。

2. ClassLoader

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

2.1 ClassLoader的作用

ClassLoader 的作用有以下 3點:

  • 將 Class 字節(jié)碼解析轉(zhuǎn)換成 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 字節(jié)流轉(zhuǎn)換為 java.lang.Class 對象。
    說明:字節(jié)流可以來源于.class文件偿洁,也可來自網(wǎng)絡(luò)或其他途徑撒汉。調(diào)用 defineClass 方法時,會對字節(jié)流進行校驗涕滋,校驗不通過會拋出 ClassFormatError 異常睬辐。該方法返回的 Class 對象還沒有 resolve(鏈接),可以顯示調(diào)用 resolveClass 方法對 Class 進行 resolve宾肺,或者在 Class 真正實例化時溯饵,由 JVM 自動執(zhí)行 resolve.

  • resolveClass

    protected final void resolveClass(Class<?> c)
    

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

  • findClass

    Class<?> findClass(String name)
    

    作用:根據(jù)類的 binary name丰刊,查找對應的 java.lang.Class 對象。
    說明:binary name 是類的全名增拥,如 String 類的 binary name 為 java.lang.String啄巧。findClass 通常和 defineClass 一起使用,下面將舉例說明二者關(guān)系跪者。
    舉例:java.net.URLClassLoader 是 ClassLoader 的子類棵帽,它重寫了 ClassLoader中的 findClass 和 defineClass 方法,我們看下 findClass 的主方法體渣玲。

    // 入?yún)?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");
        // 根據(jù)包路徑忘衍,找到該 Class 的文件資源
        Resource res = ucp.getResource(path, false);
        if (res != null) {
            try {
               // 調(diào)用 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 作為入?yún)⑶涑牵祷貙?Class 對象,但是二者在內(nèi)部實現(xiàn)上卻是不同的铅搓。loadClass 方法實現(xiàn)了 ClassLoader 的等級加載機制瑟押。我們看下 loadClass 方法的具體實現(xiàn):

    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 方法的實現(xiàn)流程主要為:

    1. 調(diào)用 findLoadedClass 方法檢查目標類是否被加載過,如果未加載過星掰,則進行下面的加載步驟
    2. 如果存在父加載器多望,則調(diào)用父加載器的loadClass 方法加載類
    3. 父加載類不存在時,調(diào)用 JVM 內(nèi)部的 ClassLoader 加載類
    4. 經(jīng)過 2氢烘,3 步驟怀偷,若還未成功加載類,則使用該 ClassLoader 自身的 findClass 方法加載類
    5. 最后根據(jù)入?yún)?resolve 判斷是否需要 resolveClass播玖,返回 Class 對象

    loadClas 默認是同步方法椎工,在實現(xiàn)自定義 ClassLoader 時,通常的做法是繼承 ClassLoader蜀踏,重寫 findClass 方法而非 loadClass 方法维蒙。這樣既能保留類加載過程的等級加載機制和線程安全性,又可實現(xiàn)從不同數(shù)據(jù)來源加載類果覆。

3. ClassLoader 的等級加載機制

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

3.1 Java 中的四層 ClassLoader

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

    Bootstarp ClassLoader 由 JVM 控制沸停,我們無法訪問到這個類。雖然它位于類記載器的頂層昭卓,但它沒有子加載器愤钾。需要通過 native 方法,來調(diào)用 Bootstap ClassLoader 來加載類候醒,如下:

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

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

    System.out.print(System.getProperty("sun.boot.class.path"));
    
    運行結(jié)果:
    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"));
    
    運行結(jié)果:
    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/ 目錄下按照類的包結(jié)構(gòu)新建目錄
    • 將編譯好的 FileClassLoader.class 丟到目錄 /JAVA_HOME/jre/lib/ext/com/eric/learning/java/_classloader 下
    • 運行命令 jar cf test.jar com,生成 test.jar
    • 現(xiàn)在就可以用 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

    繼承關(guān)系圖

    又稱系統(tǒng)類加載器血淌,App ClassLoader 負責加載項目 classpath 下的 jar 和 .class 文件,我們自己編寫的類一般有它負責加載财剖。App ClassLoader 的父加載器為 Ext ClassLoader悠夯。

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

     System.out.println(System.getProperty("java.class.path"));
    
    運行結(jié)果:
    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種形式的構(gòu)造方法:

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

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

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

    在構(gòu)造 FileClassLoader 實例時音榜,new FileClassLoader( ClassLoader ) 將拋出異常:

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

3.2 等級加載機制

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


3.3 為何采用等級加載機制

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

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

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

? 在 JVM 加載類時,會將讀取 .class 文件中的類字節(jié)碼數(shù)據(jù)翎卓,并解析拆分成 JVM 能識別的幾個部分契邀,這些不同的部分都將被存儲在 JVM 的 方法區(qū)。然后 JVM 會在 堆區(qū) 創(chuàng)建一個 java.lang.Class 對象失暴,用來封裝該類在方法區(qū)的數(shù)據(jù)坯门。 如下圖所示:

? 上文提到 .class 文件中的類字節(jié)碼數(shù)據(jù),會被 JVM 拆分成不同部分存儲在方法區(qū)逗扒,而方法區(qū)實際就是用于存儲類結(jié)構(gòu)信息的地方古戴。我們看看方法區(qū)都有哪些東西:

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

? 方法區(qū)存儲的這些類的各部分結(jié)構(gòu)信息现恼,能通過 java.lang.Class 類中的不同方法獲得,可以說 Class 對象是對類結(jié)構(gòu)數(shù)據(jù)的封裝黍檩。

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[] {};
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市刽酱,隨后出現(xiàn)的幾起案子喳逛,更是在濱河造成了極大的恐慌,老刑警劉巖棵里,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件润文,死亡現(xiàn)場離奇詭異,居然都是意外死亡殿怜,警方通過查閱死者的電腦和手機典蝌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稳捆,“玉大人赠法,你說我怎么就攤上這事∏呛唬” “怎么了砖织?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長末荐。 經(jīng)常有香客問我侧纯,道長,這世上最難降的妖魔是什么甲脏? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任眶熬,我火速辦了婚禮妹笆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘娜氏。我一直安慰自己拳缠,他們只是感情好,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布贸弥。 她就那樣靜靜地躺著窟坐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绵疲。 梳的紋絲不亂的頭發(fā)上哲鸳,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音盔憨,去河邊找鬼徙菠。 笑死,一個胖子當著我的面吹牛郁岩,可吹牛的內(nèi)容都是我干的婿奔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼驯用,長吁一口氣:“原來是場噩夢啊……” “哼脸秽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蝴乔,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驮樊,沒想到半個月后薇正,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡囚衔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年挖腰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片练湿。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡猴仑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肥哎,到底是詐尸還是另有隱情辽俗,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布篡诽,位于F島的核電站崖飘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏杈女。R本人自食惡果不足惜朱浴,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一吊圾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧翰蠢,春花似錦项乒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至趁尼,卻和暖如春埃碱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酥泞。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工砚殿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人芝囤。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓似炎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親悯姊。 傳聞我的和親對象是個殘疾皇子羡藐,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容