1. 程式人生 > >Java 類載入機制詳解

Java 類載入機制詳解

一、類載入器

  類載入器(ClassLoader),顧名思義,即載入類的東西。在我們使用一個類之前,JVM需要先將該類的位元組碼檔案(.class檔案)從磁碟、網路或其他來源載入到記憶體中,並對位元組碼進行解析生成對應的Class物件,這就是類載入器的功能。我們可以利用類載入器,實現類的動態載入。

二、類的載入機制

  在Java中,採用雙親委派機制來實現類的載入。那什麼是雙親委派機制?在Java Doc中有這樣一段描述:

The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.

 從以上描述中,我們可以總結出如下四點:

1、類的載入過程採用委託模式實現

2、每個 ClassLoader 都有一個父載入器。

3、類載入器在載入類之前會先遞迴的去嘗試使用父載入器載入。

4、虛擬機器有一個內建的啟動類載入器(bootstrap ClassLoader),該載入器沒有父載入器,但是可以作為其他載入器的父載入器。

   Java 提供三種類型的系統類載入器。第一種是啟動類載入器,由C++語言實現,屬於JVM的一部分,其作用是載入 <Java_Runtime_Home>/lib 目錄中的檔案,並且該類載入器只加載特定名稱的檔案(如 rt.jar),而不是該目錄下所有的檔案。另外兩種是 Java 語言自身實現的類載入器,包括擴充套件類載入器(ExtClassLoader)和應用類載入器(AppClassLoader),擴充套件類載入器負責載入<Java_Runtime_Home>\lib\ext目錄中或系統變數 java.ext.dirs 所指定的目錄中的檔案。應用程式類載入器負責載入使用者類路徑中的檔案。使用者可以直接使用擴充套件類載入器或系統類載入器來載入自己的類,但是使用者無法直接使用啟動類載入器,除了這兩種類載入器以外,使用者也可以自定義類載入器,載入流程如下圖所示:

  

注意:這裡父類載入器並不是通過繼承關係來實現的,而是採用組合實現的。

  我們可以通過一段程式來驗證這個過程:

?
12345678910111213public class Test {}public class TestMain {public static void main(String[] args) {ClassLoader loader = Test.class.getClassLoader();while (loader!=null){System.out.println(loader);loader = loader.getParent();}}}

  上面程式的執行結果如下所示:  

  從結果我們可以看出,預設情況下,使用者自定義的類使用 AppClassLoader 載入,AppClassLoader 的父載入器為 ExtClassLoader,但是 ExtClassLoader 的父載入器卻顯示為空,這是什麼原因呢?究其緣由,啟動類載入器屬於 JVM 的一部分,它不是由 Java 語言實現的,在 Java 中無法直接引用,所以才返回空。但如果是這樣,該怎麼實現 ExtClassLoader 與 啟動類載入器之間雙親委派機制?我們可以參考一下原始碼:

?
12345678910111213141516171819202122232425262728293031323334353637protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> 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 statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

  從原始碼可以看出,ExtClassLoader 和 AppClassLoader都繼承自 ClassLoader 類,ClassLoader 類中通過 loadClass 方法來實現雙親委派機制。整個類的載入過程可分為如下三步:

  1、查詢對應的類是否已經載入。

  2、若未載入,則判斷當前類載入器的父載入器是否為空,不為空則委託給父類去載入,否則呼叫啟動類載入器載入(findBootstrapClassOrNull 再往下會呼叫一個 native 方法)。

  3、若第二步載入失敗,則呼叫當前類載入器載入。

  通過上面這段程式,可以很清楚的看出擴充套件類載入器與啟動類載入器之間是如何實現委託模式的。

      現在,我們再驗證另一個問題。我們將剛才的Test類打成jar包,將其放置在 <Java_Runtime_Home>\lib\ext 目錄下,然後再次執行上面的程式碼,結果如下:

     現在,該類就不再通過 AppClassLoader 來載入,而是通過 ExtClassLoader 來載入了。如果我們試圖把jar包拷貝到<Java_Runtime_Home>\lib,嘗試通過啟動類載入器載入該類時,我們會發現編譯器無法識別該類,因為啟動類載入器除了指定目錄外,還必須是特定名稱的檔案才能載入。

三、自定義類載入器

  通常情況下,我們都是直接使用系統類載入器。但是,有的時候,我們也需要自定義類載入器。比如應用是通過網路來傳輸 Java 類的位元組碼,為保證安全性,這些位元組碼經過了加密處理,這時系統類載入器就無法對其進行載入,這樣則需要自定義類載入器來實現。自定義類載入器一般都是繼承自 ClassLoader 類,從上面對 loadClass 方法來分析來看,我們只需要重寫 findClass 方法即可。下面我們通過一個示例來演示自定義類載入器的流程:

?
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566package com.paddx.test.classloading;import java.io.*;/*** Created by liuxp on 16/3/12.*/public class MyClassLoader extends ClassLoader {private String root;protected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {return defineClass(name, classData, 0, classData.length);}}private byte[] loadClassData(String className) {String fileName = root + File.separatorChar+ className.replace('.', File.separatorChar) + ".class";try {InputStream ins = new FileInputStream(fileName);ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 1024;byte[] buffer = new byte[bufferSize];int length = 0;while ((length = ins.read(buffer)) != -1) {baos.write(buffer, 0, length);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}public String getRoot() {return root;}public void setRoot(String root) {this.root = root;}public static void main(String[] args)  {MyClassLoader classLoader = new MyClassLoader();classLoader.setRoot("/Users/liuxp/tmp");Class<?> testClass = null;try {testClass = classLoader.loadClass("com.paddx.test.classloading.Test");Object object = testClass.newInstance();System.out.println(object.getClass().getClassLoader());} catch (ClassNotFoundException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}}

  執行上面的程式,輸出結果如下:

  

  自定義類載入器的核心在於對位元組碼檔案的獲取,如果是加密的位元組碼則需要在該類中對檔案進行解密。由於這裡只是演示,我並未對class檔案進行加密,因此沒有解密的過程。這裡有幾點需要注意:

  1、這裡傳遞的檔名需要是類的全限定性名稱,即com.paddx.test.classloading.Test格式的,因為 defineClass 方法是按這種格式進行處理的。

  2、最好不要重寫loadClass方法,因為這樣容易破壞雙親委託模式。

  3、這類 Test 類本身可以被 AppClassLoader 類載入,因此我們不能把 com/paddx/test/classloading/Test.class 放在類路徑下。否則,由於雙親委託機制的存在,會直接導致該類由 AppClassLoader 載入,而不會通過我們自定義類載入器來載入。

四、總結

  雙親委派機制能很好地解決類載入的統一性問題。對一個 Class 物件來說,如果類載入器不同,即便是同一個位元組碼檔案,生成的 Class 物件也是不等的。也就是說,類載入器相當於 Class 物件的一個名稱空間。雙親委派機制則保證了基類都由相同的類載入器載入,這樣就避免了同一個位元組碼檔案被多次載入生成不同的 Class 物件的問題。但雙親委派機制僅僅是Java 規範所推薦的一種實現方式,它並不是強制性的要求。近年來,很多熱部署的技術都已不遵循這一規則,如 OSGi 技術就採用了一種網狀的結構,而非雙親委派機制。