Java基礎-類載入器以及載入機制

Yuicon發表於2019-01-19

我是在關於Java的面試題裡瞭解到類載入器的,在這之前從未想過Java裡類是如何被載入、解析的,一直以為只要Import就好了。事實上Java類載入器是一塊非常重要的內容,可以用在類層次劃分、OSGi、熱部署、程式碼加密等領域。即使業務上可能沒有涉及到,瞭解相關知識對排除BUG也是有幫助的。

類載入器基本概念

平時在編寫程式碼時,想使用什麼類就Import就好了,好像這些類一開始就在JVM裡了一樣,現在我們知道這是因為JVM自動為我們載入了這些類。顧名思義,類載入器的工作主要是載入Java位元組碼檔案(也就是.class檔案)到虛擬機器裡,並解析為java.lang.Class類的一個例項。到這裡,被載入的類還是不能像平時一樣直接new一個物件出來的。因為一個類總共要經歷載入、驗證、解析、初始化等4個步驟後才是Java裡的一個型別。後面幾個步驟不是本文重點,大家可以自行學習。

類載入器的組成

類載入器一共有4種,分別是引導類載入器(bootstrap class loader)、擴充套件類載入器(extensions class loader)、系統類載入器(system class loader)、自定義載入器,它們之間的載入關係如下圖所示:

其中,除了引導類載入器是用原生程式碼實現,其餘的載入器都是繼承自抽象類java.lang.ClassLoader。而且系統自帶的3個載入器都有自己的特殊之處。

引導類載入器

引導類載入器是用來載入Java的核心庫,像是java.lang包等這些Java應用必備的類都是引導類載入器載入的。載入路徑是<JAVA_HOME>lib目錄中的或者是-Xbootclasspath引數所指定的目錄中,被JVM所識別的檔案(通過名字識別,名字必須是rt.jar)。因為引導類載入器是用原生程式碼實現的,所以不能在Java程式碼中直接引用到引導類載入器。

擴充套件類載入器

顧名思義,擴充套件類載入器是用來載入Java的擴充套件類庫。載入路徑是<JAVA_HOME>libext目錄中的或者是java.ext.dirs系統變數所指定的路徑中的所有類庫。

系統類載入器

系統類載入器的載入路徑是Java應用的類路徑(CLASSPATH),也就是說在沒有自定義載入器的情況下,Java應用的類都是由系統類載入器載入的。而且該載入器可以用ClassLoader類的getSystemClassLoader()方法直接獲取到。

除了引導類載入器,每個載入器都有一個父載入器。比如載入器A載入了載入器B,那麼載入器A就是載入器B的父載入器,可以通過java.lang.ClassLoadergetParent()方法獲取父載入器,而且Java中每個Class物件都維護著一個載入器引用,可以通過getClassLoader()方法獲取載入該類的載入器。

例如下面這段程式碼:

public class Main {

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

}

這裡輸出了Main類的載入器與其所有的父載入器,執行結果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d

Process finished with exit code 0

我們看到Main類的載入器是系統類載入器,它的父載入器是擴充套件類載入器。擴充套件類載入器的父載入器應該是引導類載入器才對,這裡沒有輸出是因為有些JDK的實現裡在父載入器為引導類載入器的情況下是返回null的。

雙親委託模式

第一次看到雙親委託模式這個詞的時候就感覺意義不明,完全不知道是什麼意思。在瞭解了載入器的載入過程之後,才發現是一種代理模式。

以上文中的Main類的載入過程為例,它的載入器為系統類載入器。但是系統類載入器不會直接去載入這個類,而是先委託給它的父載入器,也就是擴充套件類載入器。同樣,擴充套件類載入器也會先委託給它的父載入器,一直委託到引導類載入器才開始真正的嘗試載入,如果載入失敗就返回由發出委託的載入器嘗試載入。

這樣做的目的是為了保護Java核心庫和保持型別安全。因為在JVM中判斷兩個類是否相同,不僅僅是看它們的全名是否相同,還要判斷它們的載入器是否相同。通過雙親委託模式就能保證每次載入核心庫的載入器都是引導類載入器,從而防止出現類似於多個java.lang.Object型別這種情況。

自定義載入器

編寫自定義載入器並不困難,只要繼承抽象類java.lang.ClassLoader並覆蓋findClass(String name)方法就行了。不建議覆蓋 loadClass(String name)方法,因為這個方法裡面封裝了前面提到的雙親委託模式,覆蓋可能會導致該模式失效。

// 原始碼來自 https://www.ibm.com/developerworks/cn/java/j-lo-classloader
public class FileSystemClassLoader extends ClassLoader { 
 
   private String rootDir; 
 
   public FileSystemClassLoader(String rootDir) { 
       this.rootDir = rootDir; 
   } 
 
   protected Class<?> findClass(String name) throws ClassNotFoundException { 
       byte[] classData = getClassData(name); 
       if (classData == null) { 
           throw new ClassNotFoundException(); 
       } 
       else { 
           return defineClass(name, classData, 0, classData.length); 
       } 
   } 
 
   private byte[] getClassData(String className) { 
       String path = classNameToPath(className); 
       try { 
           InputStream ins = new FileInputStream(path); 
           ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
           int bufferSize = 4096; 
           byte[] buffer = new byte[bufferSize]; 
           int bytesNumRead = 0; 
           while ((bytesNumRead = ins.read(buffer)) != -1) { 
               baos.write(buffer, 0, bytesNumRead); 
           } 
           return baos.toByteArray(); 
       } catch (IOException e) { 
           e.printStackTrace(); 
       } 
       return null; 
   } 
 
   private String classNameToPath(String className) { 
       return rootDir + File.separatorChar 
               + className.replace(`.`, File.separatorChar) + ".class"; 
   } 
}

參考資料

Java類載入機制與Tomcat類載入器架構

深入探討 Java 類載入器

相關文章