雖然之前也看過jvm相關的書籍,但是都是概念層次上的理解。今天特地花一天時間研究了下類加載器,感覺上是沒有那麼生疏了,但也只是冰山一角,索性就不完整地分析一番吧。內容有些長,可使用目錄快速查閱。
類加載器
簡單說下JVM預定義的三種類型的類加載器,這個也算是老生常談了。當JVM啓動一個項目的時候,它將缺省使用以下三種類型的類加載器:
1. 啓動(Bootstrap)類加載器:負責裝載<Java_Home>/lib
下面的核心類庫或-Xbootclasspath
選項指定的jar包。由native方法實現加載過程,程序無法直接獲取到該類加載器,無法對其進行任何操作。
2. 擴展(Extension)類加載器:擴展類加載器由sun.misc.Launcher.ExtClassLoader
實現的。負責加載<Java_Home>/lib/ext
或者由系統變量-Djava.ext.dir
指定位置中的類庫。程序可以訪問並使用擴展類加載器。
3. 系統(System)類加載器:系統類加載器是由sun.misc.Launcher.AppClassLoader
實現的,也叫應用程序類加載器。負責加載系統類路徑-classpath
或-Djava.class.path
變量所指的目錄下的類庫。程序可以訪問並使用系統類加載器。
雙親委派類加載機制
類加載器的父子關係
三種類加載器的父子關係如圖所示
注意這兒的父子並不是繼承的意思,它們都是ClassLoader
抽象類的實現,因此都含有一個ClassLoader parent
成員變量,該變量指向其父加載器,類似單向鏈表。
雙親委派源碼實現
委派關係也被稱爲代理,我們來看看代碼,loadClass
是抽象類ClassLoader
中的類加載的核心方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 若本加載器之前是否已加載過,直接取緩存,native方法實現
Class c = findLoadedClass(name);
if (c == null) {
try {
// 只要有父加載器就先委派父加載器來加載
if (parent != null) {
// 注意此處遞歸調用
c = parent.loadClass(name, false);
} else {
// ext的parent爲null,因爲Bootstrap是無法被程序被訪問的,默認parent爲null時其父加載器就是Bootstrap
// 此時直接用native方法調用啓動類加載加載,若找不到則拋異常
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 對ClassNotFoundException不做處理,僅用作退出遞歸
}
if (c == null) {
// 如果父加載器無法加載那麼就在本類加載器的範圍內進行查找
// findClass找到class文件後將調用defineClass方法把字節碼導入方法區,同時緩存結果
c = findClass(name);
}
}
// 是否解析,默認false
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看出所謂的雙親委派的本質就是這兩句遞歸代碼:
if (parent != null) {
c = parent.loadClass(name, false);
}
加載成功就得到Class對象c,失敗就拋異常然後前一級方法用catch抓住並忽略,再進行當前類加載器的findClass()
操作,如此反覆。
注意
1. 出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類
2. 類加載後將進入連接(link)階段,它包含驗證、準備、解析,resolve參數決定是否執行解析階段,jvm規範並沒有嚴格指定該階段的執行時刻
3. 由於先使用findLoadedClass()
查找緩存,相同的類只會被加載一次
用戶自定義類加載器
當你自己寫一個類實現了ClassLoader
後,那麼它就是用戶自定義類加載器了。實例化自定義類加載器時,若不指定父類加載器(不把父ClassLoader傳入構造函數)的情況下,默認採用系統類加載器(AppClassLoader)。對應的無參默認構造函數實現如下:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
它將調用有參構造函數,將getSystemClassLoader()
取到的系統類加載器作爲parent傳入(最後一節詳述)。因此用戶自定義類加載器也可以通過雙親委派的方式獲取到那3個類加載器加載的類對象了。
當實現自定義類加載器時不應重寫loadClass()
,除非你不需要雙親委派機制。要重寫的是findClass()
的邏輯,也就是尋找並加載類的方式。
使用自定義類加載器獲取到的Class對象需通過newInstance()獲取實例,要比較具有相同類全限定名的兩個Class對象是否是同一個,取決於是否是同一類加載器加載了它們,也就是調用defineClass()
的那個類加載器,而非之前委派的類加載器。
常用方法分析
java.lang.Class
對象的方法
● Class<?> forName(……)
這是手動加載類的常見方式,在Class
類中有兩個重載:
public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
第二個構造函數指定了父類加載器,這兒可能要有疑問了,第一個方法默認使用哪個類加載器來加載的呢?我們來看下具體實現:
public static Class<?> forName(String className)
throws ClassNotFoundException {
// 使用native方法獲取調用類的Class對象
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
其中getClassLoader(caller)
設置了所使用的類加載器,繼續看其實現:
static ClassLoader getClassLoader(Class<?> caller) {
if (caller == null) {
return null;
}
return caller.getClassLoader0();
}
}
這段代碼的官方註解是“返回caller的類加載器”,即native方法getClassLoader0()
返回調用者的類加載器。也就是說假設在A類裏執行forName(String className)
,那麼所使用的ClassLoader就是加載A的ClassLoader。
提示
forName0()
本質還是調用ClassLoader
的loadClass()
來加載類。
● ClassLoader getClassLoader()
該方法用於獲取加載某Class對象的類加載器,可是通過實例或類對象來獲取:
(new A()).getClass().getClassLoader()
A.class.getClassLoader()
各種獲取類信息的方法
反射得到Class對象後通過以下方法獲取類信息:
Field[] getDeclaredFields()
Class[] getDeclaredClasses()
Method[] getDeclaredMethods()
等等
詳情可查閱javadoc或查看源碼
java.lang.ClassLoader
對象的方法
● ClassLoader getParent()
獲取父ClassLoader
● Class loadClass(String)
顯式調用該方法來進行類加載,傳入類全限定名
● URL getResource(String)
獲取具有給定名稱的資源定位符。資源可以是任何數據,名稱須以“/”分離路徑名。實際調用findResource()
方法,該方法無實現,需子類繼承實現。
● InputStream getResourceAsStream(String)
獲取可以讀取資源的InputStream輸入流,實際上就是用上面的方法獲取到URL後調用url.openStream()
得到 InputStream。
● ClassLoader getSystemClassLoader()
這是一個靜態方法,通過ClassLoader.getSystemClassLoader()
便可獲取到系統類加載器AppClassLoader, 和調用類無關。具體實現見最後一小節。
URLClassLoader
概述
ClassLoader
只是一個抽象類,很多方法是空的需要自己去實現,比如 findClass()
、findResource()
等。而java提供了java.net.URLClassLoader
這個實現類,適用於多種應用場景。
之前提到的AppClassLoader
、ExtClassLoader
都是URLClassLoader
的子類,自定義類加載器推薦直接繼承它。
來看下javadoc中的描述:
該類加載器用於從一組URL路徑(指向JAR包或目錄)中加載類和資源。約定使用以 ‘/’結束的URL來表示目錄。如果不是以該字符結束,則認爲該URL指向一個JAR文件。
構造函數
URLClassLoader
接受一個URL數組爲參數,它將在這些提供的路徑下加載所需要的類,對應的主要構造函數有
public URLClassLoader(URL[] urls)
URLClassLoader(URL[] urls, ClassLoader parent)
getURLs()
方法
使用URL[] getURLs()
方法可以獲取URL路徑,參考代碼:
public static void main(String[] args) {
URL[] urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
for (URL url : urls) {
System.out.println(url);
}
}
// file:/D:/Workbench/Test/bin/
加載方式
在findClass()
中其使用了URLClassPath
類中的Loader
類來加載類文件和資源。URLClassPath
類中定義了兩個Loader
類的實現,分別是FileLoader
和JarLoader
類,顧名思義前者用於加載目錄中的類和資源,後者是加載jar包中的類和資源。Loader
類默認已經實現getResource()
方法,即從網絡URL地址加載jar包然後使用JarLoader
完成後續加載,而兩個實現類不過是重寫了該方法。
那URLClassPath
是如何選擇使用正確的Loader
的呢?答案是——根據URL格式而定。下面是刪減過的核心代碼,簡單易懂。
private Loader getLoader(final URL url)
{
String s = url.getFile();
// 以"/"結尾時,若url協議爲"file"則使用FileLoader加載本地文件
// 否則使用默認的Loader加載網絡url
if(s != null && s.endsWith("/"))
{
if("file".equals(url.getProtocol()))
return new FileLoader(url);
else
return new Loader(url);
} else {
// 非"/"結尾則使用JarLoader
return new JarLoader(url, jarHandler, lmap);
}
}
getSystemClassLoader()
方法的實現
追溯getSystemClassLoader()
的源碼可以發現其實質上是通過sun.misc.Launcher
實例獲取返回其成員變量loader
的。那這個loader
是何時賦值的呢?我們來看下它的構造函數(刪減了不相關的內容):
public Launcher()
{
ExtClassLoader extclassloader;
try
{
// 創建並初始化擴展類加載器ExtClassLoader
extclassloader = ExtClassLoader.getExtClassLoader();
}
catch(IOException ioexception)
{
throw new InternalError("Could not create extension class loader");
}
try
{
// 創建並初始化系統類加載器AppClassLoader,設置其父類加載器爲ext,最後傳給loader
loader = AppClassLoader.getAppClassLoader(extclassloader);
}
catch(IOException ioexception1)
{
throw new InternalError("Could not create application class loader");
}
// 默認將線程上下文類加載器設置爲AppClassLoader
// 相關信息見另一篇博文
Thread.currentThread().setContextClassLoader(loader);
}
可以看到Launcher
初始化時創建生成了ExtClassLoader
和AppClassLoader
,並將線程上下文類加載器默認設置爲了AppClassLoader
。雖然沒去看jvm的源碼,但我推測jvm可能就是通過創建Launcher
實例來完成擴展和系統類加載器的創建的,而啓動(Bootstrap)類加載器的創建則是另外調用本地方法完成的。
很明顯,getSystemClassLoader()
返回的loader就是AppClassLoader
無誤,這兒我們也發現了線程上下文類加載器賦值處,具體有關線程上下文類加載器的學習請參考底部的另一篇博文。
總結
通常需要你自己寫類加載器的場景不多,但通過上述對類加載器的分析研究至少可以讓你瞭解jvm的底層實現機制以及熟悉反射的實現方式。我個人的風格就是知其然知其所以然,在我理解範圍內的知識我都有興趣去研究。之前總是花一整段時間去啃下難點後就置之不理了,工作後才養成這種常記筆記的習慣,自己總結梳理後的確比看別人的文章要來得更深刻更透徹,望繼續保持!
延伸閱讀:真正理解線程上下文類加載器(多案例分析)