雙親委派策略與自定義類加載器
類加載器
類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之後就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class類的一個實例。
任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機中的唯一性。
從JVM的角度來說,有兩種類加載器:啟動類加載器,其他類加載器
- 啟動類加載器(Bootstrap ClassLoader):由C++語言實現(針對HotSpot),負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到內存中。
- 其他類加載器:由Java語言實現,繼承自抽象類ClassLoader。如:
- 擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變量指定的路徑中的所有類庫。
- 應用程序類加載器(Application ClassLoader)。負責加載用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器。一般情況,如果我們沒有自定義類加載器默認就是用這個加載器。
應用程序由這三種類加載器互相配合進行加載的,如果有必須,還可以加入自己定義的類加載器。這些類加載器之間的關系一般如下圖:
上圖中展示的類加載器之間的層次關系,就稱為類加載器的雙親委派模型(Parents Delegation Model)。
雙親委派模型
雙親委派模型除了Bootstrap ClassLoader外,其余的類加載器都要有自己的父加載器。子加載器通過組合來復用父加載器的代碼,而不是使用繼承。
雙親委派模型的過程:
- 當Application ClassLoader 收到一個類加載請求時,他首先不會自己去嘗試加載這個類,而是將這個請求委派給父類加載器Extension ClassLoader去完成。
- 當Extension ClassLoader收到一個類加載請求時,他首先也不會自己去嘗試加載這個類,而是將請求委派給父類加載器Bootstrap ClassLoader去完成。
- 如果Bootstrap ClassLoader加載失敗(在<JAVA_HOME>\lib中未找到所需類),就會讓Extension ClassLoader嘗試加載。
- 如果Extension ClassLoader也加載失敗,就會使用Application ClassLoader加載。
- 如果Application ClassLoader也加載失敗,就會使用自定義加載器去嘗試加載。
- 如果均加載失敗,就會拋出ClassNotFoundException異常。
其中,進行類加載的請求委派過程是一個從下到上的過程;進行嘗試類加載的過程是一個從上到下的過程。
雙親委派模型的優點
Java類伴隨其類加載器具備了帶有優先級的層次關系,確保了在各種加載環境的加載順序。 保證了運行的安全性,防止不可信類扮演可信任的類。
如,Java中的Object類,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啟動類加載器進行加載,因此Object在各種類加載環境中都是同一個類。如果不采用雙親委派模型,那麽由各個類加載器自己取加載的話,那麽系統中會存在多種不同的Object類。
雙親委派模型的破壞
第一次破壞
由於雙親委派模型是在JDK1.2之後才被引入的,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,面對已經存在的用戶自定義類加載器的實現代碼,Java設計者引入雙親委派模型時不得不做出一些妥協。在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,因為虛擬機在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法唯一邏輯就是去調用自己的loadClass()。
第二次破壞
雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類的同一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以稱為“基礎”,是因為它們總是作為被用戶代碼調用的API,但世事往往沒有絕對的完美。
如果基礎類又要調用回用戶的代碼,那該麽辦?
一個典型的例子就是JNDI服務,JNDI現在已經是Java的標準服務,
它的代碼由啟動類加載器去加載(在JDK1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者的代碼,但啟動類加載器不可能“認識”這些代碼。
為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,他將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
有了線程上下文加載器,JNDI服務就可以使用它去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行為實際上就是打通了雙親委派模型層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
第三次破壞
雙親委派模型的第三次“被破壞”是由於用戶對程序動態性的追求導致的,這裏所說的“動態性”指的是當前一些非常“熱門”的名詞:代碼熱替換、模塊熱部署等,簡答的說就是機器不用重啟,只要部署上就能用。
OSGi實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每一個程序模塊(Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。在OSGi幻境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加復雜的網狀結構,當受到類加載請求時,OSGi將按照下面的順序進行類搜索:
1)將java.*開頭的類委派給父類加載器加載。
2)否則,將委派列表名單內的類委派給父類加載器加載。
3)否則,將Import列表中的類委派給Export這個類的Bundle的類加載器加載。
4)否則,查找當前Bundle的ClassPath,使用自己的類加載器加載。
5)否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載。
6)否則,查找Dynamic Import列表的Bundle,委派給對應Bundle的類加載器加載。
7)否則,類加載器失敗
自定義類加載器
幾個函數
loadClass
loadClass的默認實現如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
再看看loadClass(String name, boolean resolve)函數:
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(String, boolean)函數即實現了雙親委派模型!整個大致過程如下:
- 首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再加載,直接返回。
- 如果此類沒有加載過,那麽,再判斷一下是否有父加載器;如果有父加載器,則由父加載器加載(即調用parent.loadClass(name, false);).或者是調用bootstrap類加載器來加載。
- 如果父加載器及bootstrap類加載器都沒有找到指定的類,那麽調用當前類加載器的findClass方法來完成類加載。
換句話說,如果自定義類加載器,就必須重寫findClass方法!
findClass
findClass的默認實現如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以看出,抽象類ClassLoader的findClass函數默認是拋出異常的。而前面我們知道,loadClass在父加載器無法加載類的時候,就會調用我們自定義的類加載器中的findeClass函數,因此我們必須要在loadClass這個函數裏面實現將一個指定類名稱轉換為Class對象.
defineClass
defineClass主要的功能是:將一個字節數組轉為Class對象,這個字節數組是class文件讀取後最終的字節數組。如,假設class文件是加密過的,則需要解密後作為形參傳入defineClass函數。
defineClass的默認實現如下:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError {
return defineClass(name, b, off, len, null);
}
函數調用過程
函數調用過程如下圖所示:
簡單示例
項目參考自:https://www.cnblogs.com/wxd0108/p/6681618.html
定義一個待加載的普通Java類:Test.java。放在com.luis.test包下:
package com.luis.test;
public class Test {
public void hello() {
System.out.println(我是由 " + getClass().getClassLoader().getClass()+ " 加載進來的");
}
}
待Test.java編譯後,請把Test.class文件拷貝走,再將Test.java刪除。若把Test.class存放在當前項目中,根據雙親委派模型可知,會通過sun.misc.Launcher$AppClassLoader類加載器加載。為了讓我們自定義的類加載器加載,我們把Test.class文件放入到其他目錄。
public class Main {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
};
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class clazz = classLoader.loadClass("com.luis.test.Test");
Object obj = clazz.newInstance();
Method helloMethod = clazz.getDeclaredMethod("hello", null);
helloMethod.invoke(obj, null);
}
}
運行結果如下:
我是由 class Main$MyClassLoader 加載進來的
本文參考了:
https://www.jianshu.com/p/5f79217f2e18
https://www.cnblogs.com/wxd0108/p/6681618.html
https://www.cnblogs.com/louistz/p/6295917.html
https://blog.csdn.net/zhangcanyan/article/details/78993959
雙親委派策略與自定義類加載器