所謂的類加載器(Class Loader)就是加載Java類到Java虛擬機中的,前面《面試官,不要再問我「Java虛擬機類加載機制」了》中已經介紹了具體加載class文件的機制。本篇文章咱們重點介紹加載器和雙親委派機制。java
在JVM中有三類ClassLoader構成:啓動類(或根類)加載器(Bootstrap ClassLoader)、擴展類加載器(ExtClassLoader)、應用類加載器(AppClassLoader)。不一樣的類加載器負責不一樣區域的類的加載。mysql
啓動類加載器:這個加載器不是一個Java類,而是由底層的c++實現,負責將存放在JAVA_HOME下lib目錄中的類庫,好比rt.jar。所以,啓動類加載器不屬於Java類庫,沒法被Java程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給引導類加載器,那直接使用null代替便可。c++
擴展類加載器:由sun.misc.Launcher$ExtClassLoader實現,負責加載JAVA_HOME下libext目錄下的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。面試
應用類加載器:由sun.misc.Launcher$AppClassLoader實現的。因爲這個類加載器是ClassLoader中的getSystemClassLoader方法的返回值,因此也叫系統類加載器。它負責加載用戶類路徑上所指定的類庫,能夠被直接使用。若是未自定義類加載器,默認爲該類加載器。sql
能夠經過這種方式打印加載路徑及相關jar:數據庫
System.out.println("boot:" + System.getProperty("sun.boot.class.path"));
System.out.println("ext:" + System.getProperty("java.ext.dirs"));
System.out.println("app:" + System.getProperty("java.class.path"));複製代碼
在打印的日誌中,能夠看到詳細的路徑以及路徑下面都包含了哪些類庫。因爲打印內容較多,這裏就不展現了。數組
除啓動類加載器外,擴展類加載器和應用類加載器都是經過類sun.misc.Launcher進行初始化,而Launcher類則由根類加載器進行加載。相關代碼以下:安全
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//初始化擴展類加載器,構造函數沒有入參,沒法獲取啓動類加載器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//初始化應用類加載器,入參爲擴展類加載器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 設置上下文類加載器
Thread.currentThread().setContextClassLoader(this.loader);
//...
}複製代碼
雙親委派模型:當一個類加載器接收到類加載請求時,會先請求其父類加載器加載,依次遞歸,當父類加載器沒法找到該類時(根據類的全限定名稱),子類加載器纔會嘗試去加載。微信
雙親委派中的父子關係通常不會以繼承的方式來實現,而都是使用組合的關係來複用父加載器的代碼。併發
經過編寫測試代碼,進行debug,能夠發現雙親委派過程當中不一樣類加載器之間的組合關係。
而這一過程借用一張時序圖來查看會更加清晰。
ClassLoader類是一個抽象類,但卻沒有包含任何抽象方法。繼承ClassLoader類並重寫findClass方法即可實現自定義類加載器。但若是破壞上面所述的雙親委派模型來實現自定義類加載器,則須要繼承ClassLoader類並重寫loadClass方法和findClass方法。
ClassLoader類的部分源碼以下:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
//進行類加載操做時首先要加鎖,避免併發加載
synchronized (getClassLoadingLock(name)) {
//首先判斷指定類是否已經被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//若是當前類沒有被加載且父類加載器不爲null,則請求父類加載器進行加載操做
c = parent.loadClass(name, false);
} else {
//若是當前類沒有被加載且父類加載器爲null,則請求根類加載器進行加載操做
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
//若是父類加載器加載失敗,則由當前類加載器進行加載,
c = findClass(name);
//進行一些統計操做
// ...
}
}
//初始化該類
if (resolve) {
resolveClass(c);
}
return c;
}
}複製代碼
上面代碼中也提現了不一樣類加載器之間的層級及組合關係。
雙親委派模型是爲了保證Java核心庫的類型安全。全部Java應用都至少須要引用java.lang.Object類,在運行時這個類須要被加載到Java虛擬機中。若是該加載過程由自定義類加載器來完成,可能就會存在多個版本的java.lang.Object類,並且這些類之間是不兼容的。
經過雙親委派模型,對於Java核心庫的類的加載工做由啓動類加載器來統一完成,保證了Java應用所使用的都是同一個版本的Java核心庫的類,是互相兼容的。
子類加載器都保留了父類加載器的引用。但若是父類加載器加載的類須要訪問子類加載器加載的類該如何處理?最經典的場景就是JDBC的加載。
JDBC是Java制定的一套訪問數據庫的標準接口,它包含在Java基礎類庫中,由根類加載器加載。而各個數據庫廠商的實現類庫是做爲第三方依賴引入使用的,這部分實現類庫是由應用類加載器進行加載的。
獲取Mysql鏈接的代碼:
//加載驅動程序
Class.forName("com.mysql.jdbc.Driver");
//鏈接數據庫
Connection conn = DriverManager.getConnection(url, user, password);複製代碼
DriverManager由啓動類加載器加載,它使用到的數據庫驅動(com.mysql.jdbc.Driver)是由應用類加載器加載的,這就是典型的由父類加載器加載的類須要訪問由子類加載器加載的類。
這一過程的實現,看DriverManager類的源碼:
//創建數據庫鏈接底層方法
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//獲取調用者的類加載器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
//由啓動類加載器加載的類,該值爲null,使用上下文類加載器
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
//...
for(DriverInfo aDriver : registeredDrivers) {
//使用上下文類加載器去加載驅動
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
//加載成功,則進行鏈接
Connection con = aDriver.driver.connect(url, info);
//...
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}
//...
}
}複製代碼
在上面的代碼中留意改行代碼:
callerCL = Thread.currentThread().getContextClassLoader();複製代碼
這行代碼從當前線程中獲取ContextClassLoader,而ContextClassLoader在哪裏設置呢?就是在上面的Launcher源碼中設置的:
// 設置上下文類加載器
Thread.currentThread().setContextClassLoader(this.loader);複製代碼
這樣一來,所謂的上下文類加載器本質上就是應用類加載器。所以,上下文類加載器只是爲了解決類的逆向訪問提出來的一個概念,並非一個全新的類加載器,本質上是應用類加載器。
自定義類加載器只須要繼承java.lang.ClassLoader類,而後重寫findClass(String name)方法便可,在方法中指明如何獲取類的字節碼流。
若是要破壞雙親委派規範的話,還需重寫loadClass方法(雙親委派的具體邏輯實現)。但不建議這麼作。
public class ClassLoaderTest extends ClassLoader {
private String classPath;
public ClassLoaderTest(String classPath) {
this.classPath = classPath;
}
/**
* 編寫findClass方法的邏輯
*
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 獲取類的class文件字節數組
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
// 生成class對象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 編寫獲取class文件並轉換爲字節碼流的邏輯
*
* @param className
* @return
*/
private byte[] getClassData(String className) {
// 讀取類文件的字節
String path = classNameToPath(className);
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
// 讀取類文件的字節碼
while ((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 類文件的徹底路徑
*
* @param className
* @return
*/
private String classNameToPath(String className) {
return classPath + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
public static void main(String[] args) {
String classPath = "/Users/zzs/my/article/projects/java-stream/src/main/java/";
ClassLoaderTest loader = new ClassLoaderTest(classPath);
try {
//加載指定的class文件
Class<?> object1 = loader.loadClass("com.secbro2.classload.SubClass");
System.out.println(object1.newInstance().toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}複製代碼
打印結果:
SuperClass static init
SubClass static init
com.secbro2.classload.SubClass@5451c3a8複製代碼
關於SuperClass和SubClass在上篇文章《面試官,不要再問我「Java虛擬機類加載機制」了》已經貼過代碼,這裏就再也不貼出了。
經過上面的代碼能夠看出,主要重寫了findClass獲取class的路徑便實現了自定義的類加載器。
那麼,什麼場景會用到自定義類加載器呢?當JDK提供的類加載器實現沒法知足咱們的需求時,才須要本身實現類加載器。好比,OSGi、代碼熱部署等領域。
以上類加載器模型爲Java8之前版本,在Java9中類加載器已經發生了變化。在這裏主要簡單介紹一下相關模型的變化,具體變化細節就再也不這裏展開了。
java9中目錄的改變。
Java9中類加載器的改變。
在java9中,應用程序類加載器能夠委託給平臺類加載器以及啓動類加載器;平臺類加載器能夠委託給啓動類加載器和應用程序類加載器。
在java9中,啓動類加載器是由類庫和代碼在虛擬機中實現的。爲了向後兼容,在程序中仍然由null表示。例如,Object.class.getClassLoader()仍然返回null。可是,並非全部的JavaSE平臺和JDK模塊都由啓動類加載器加載。
舉幾個例子,啓動類加載器加載的模塊是java.base,java.logging,java.prefs和java.desktop。其餘JavaSE平臺和JDK模塊由平臺類加載器和應用程序類加載器加載。
java9中再也不支持用於指定引導類路徑,-Xbootclasspath和-Xbootclasspath/p選項以及系統屬性sun.boot.class.path。-Xbootclasspath/a選項仍然受支持,其值存儲在jdk.boot.class.path.append的系統屬性中。
java9再也不支持擴展機制。可是,它將擴展類加載器保留在名爲平臺類加載器的新名稱下。ClassLoader類包含一個名爲getPlatformClassLoader()的靜態方法,該方法返回對平臺類加載器的引用。
本篇文章主要基於java8介紹了Java虛擬機類加載器及雙親委派機制,和Java8中的一些變化。其中,java9中更深層次的變化,你們能夠進一步研究一下。該系列持續更新中,歡迎關注微信公衆號「程序新視界」。
原文連接:《Java虛擬機類加載器及雙親委派機制》
《面試官》系列文章: