在編寫 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 方法的實現流程主要為:
- 調用 findLoadedClass 方法檢查目標類是否被加載過,如果未加載過,則進行下面的加載步驟
- 如果存在父加載器,則調用父加載器的loadClass 方法加載類
- 父加載類不存在時,調用 JVM 內部的 ClassLoader 加載類
- 經過 2,3 步驟,若還未成功加載類,則使用該 ClassLoader 自身的 findClass 方法加載類
- 最后根據入參 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[] {};
}
}