在編寫 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)流程主要為:
- 調(diào)用 findLoadedClass 方法檢查目標類是否被加載過,如果未加載過星掰,則進行下面的加載步驟
- 如果存在父加載器多望,則調(diào)用父加載器的loadClass 方法加載類
- 父加載類不存在時,調(diào)用 JVM 內(nèi)部的 ClassLoader 加載類
- 經(jīng)過 2氢烘,3 步驟怀偷,若還未成功加載類,則使用該 ClassLoader 自身的 findClass 方法加載類
- 最后根據(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
又稱系統(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[] {};
}
}