深入JVM:(十)類載入器
類載入器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態載入到 Java 虛擬機器中並執行。
類載入器(class loader)
用來載入 Java 類到 Java 虛擬機器中。一般來說,Java 虛擬機器使用 Java 類的方式如下:Java 源程式(.java 檔案)在經過 Java 編譯器編譯之後就被轉換成 Java 位元組程式碼(.class 檔案)。類載入器負責讀取 Java 位元組程式碼,並轉換成 java.lang.Class類的一個例項。每個這樣的例項用來表示一個 Java 類。通過此例項的 newInstance()方法就可以創建出該類的一個物件。實際的情況可能更加複雜,比如 Java 位元組程式碼可能是通過工具動態生成的,也可能是通過網路下載的。
雙親委派模型
如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

類載入器雙親委派模型.png
雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係一般不會以 繼承(Inheritance)
的關係來實現,而是都使用 組合(Composition)
關係來複用父載入器的程式碼。
使用雙親委派模型來組織類載入器之間的關係,有一個顯而易見的好處就是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。
雙親委派模型對於保證Java程式的穩定運作很重要,但它的實現卻非常簡單,實現雙親委派的程式碼都集中在java.lang.ClassLoader的loadClass()方法之中,如程式碼清單7-10所示,邏輯清晰易懂:先檢查是否已經被載入過,若沒有載入則呼叫父載入器的loadClass()方法,若父載入器為空則預設使用啟動類載入器作為父載入器。如果父類載入失敗,丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 先檢查類是否已經載入過 Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //如果父類載入器丟擲ClassNotFoundException //說明父類載入器無法完成載入請求 } if (c == null) { //在父類載入器無法載入的時候 //再呼叫本身的findClass方法來進行類載入 c = findClass(name); } } return c; }
Java 中的類載入器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類載入器主要有下面三個:
啟動類載入器(Bootstrap ClassLoader)
這個類載入器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。
String類就是rt.jar裡面提供的,這個類我們經常用,下面我們看下String類的類載入器是什麼。
public static void main(String[] args) { ClassLoader cl = String.class.getClassLoader(); System.out.println(cl); }
執行結果如下。
null
可知由於BootstrapClassloader對Java不可見,所以返回了null,我們也可以通過看某一個類的載入器是否為null來作為判斷該類是不是使用BootstrapClassloader進行載入的依據。另外ExtClassLoader的父載入器返回的null,那是否說明ExtClassLoader的父載入器是BootstrapClassloader呢?
擴充套件類載入器(Extension ClassLoader)
這個載入器由sun.misc.Launcher $ExtClassLoader實現,它負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
應用程式類載入器(Application ClassLoader)
這個類載入器由sun.misc.Launcher $App-ClassLoader實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
呼叫 ClassLoader.getSystemClassLoader()
可以獲取該類載入器。如果沒有特別指定,則使用者自定義的任何類載入器都將該類載入器作為它的父載入器,這點通過java.lang.ClassLoader的無參建構函式可以證明,程式碼如下
public abstract class ClassLoader { protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } private static Void checkCreateClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } return null; } private static synchronized void initSystemClassLoader() { if (!sclSet) { if (scl != null) throw new IllegalStateException("recursive invocation"); sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l != null) { Throwable oops = null; //從launcher中獲取classloader scl = l.getClassLoader(); try { scl = AccessController.doPrivileged( new SystemClassLoaderAction(scl)); } catch (PrivilegedActionException pae) { oops = pae.getCause(); if (oops instanceof InvocationTargetException) { oops = oops.getCause(); } } if (oops != null) { if (oops instanceof Error) { throw (Error) oops; } else { // wrap the exception throw new Error(oops); } } } sclSet = true; } }
下面看一下Launcher中的實現:
public class Launcher { private ClassLoader loader; public Launcher() { ... try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } ... } static class AppClassLoader extends URLClassLoader { final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this); public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { final String var1 = System.getProperty("java.class.path"); final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1); return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() { public Launcher.AppClassLoader run() { URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2); return new Launcher.AppClassLoader(var1x, var0); } }); } } }
從AppClassLoader的建構函式中可以看到,通過獲得classpath載入路徑來建立應用程式載入器(看到這是不是終於知道環境變數裡的java.class.path幹嘛用的了)
另外我們寫的含有main函式的類的載入就是使用該類載入器進行載入的,證明如下:
public static void main(String[] args) { ClassLoader cl = App2.class.getClassLoader(); System.out.println(cl); System.out.println(cl.getParent()); }
執行結果如下。
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
並且可以看出AppClassLoader的父載入器是ExtClassLoader
除了系統提供的類載入器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類載入器,以滿足一些特殊的需求。
自定義類載入器
其中有如下三個比較重要的方法
方法 | 說明 |
---|---|
defineClass(String name, byte[] b, int off, int len) | 把位元組陣列 b中的內容轉換成 Java 類,該位元組陣列可以看成是二進位制流位元組組成的檔案,返回的結果是 java.lang.Class 類的例項。這個方法被宣告為 final的。 |
loadClass(String name) | 上文中已貼出原始碼,實現了雙親委派模型,呼叫 findClass() 執行類載入動作,返回的是 java.lang.Class 類的例項。 |
findClass(String name) | 通過傳入的類全限定名name來獲取對應的類,返回的是 java.lang.Class 類的例項,該類沒有提供具體的實現,開發者在自定義類載入器時需重用此方法,在實現此方法時需呼叫 defineClass(String name, byte[] b, int off, int len) 方法。 |
我們可以容易地意識到自定義類載入器有以下兩種方式:
- 採用雙親委派模型 :繼承ClassLoader類,只需重寫其的
findClass(String name)
方法,而不需重寫loadClass(String name)
方法。 - 破壞雙親委派模型 :繼承ClassLoader類,需要整個重寫實現了雙親委派模型邏輯的
loadClass(String name)
方法。
下面我們來實現一個自定義類載入器,用來載入儲存在檔案系統上的 Java 位元組程式碼。
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } @Override 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"; } }
類 FileSystemClassLoader的 findClass()
方法首先根據類的全名在硬碟上查詢類的位元組程式碼檔案(.class 檔案),然後讀取該檔案內容,最後通過 defineClass()方法來把這些位元組程式碼轉換成 java.lang.Class
類的例項。