Java類載入器(一)——類載入器層次與模型
類載入器
??虛擬機器設計團隊把類載入階段中的“通過一個類的全限定名來擷取描述此類的二進位位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去擷取所需要的類。實現這個動作的代碼模組稱為“類載入器”。
類載入器層次(等級)
??從JVM的角度來講,只存在兩種不同的類載入器。
??第一類是啟動類載入器(Bootstrap ClassLoader):這個類載入器主要載入JVM自身工作需要的類。這個類載入器由C++語言實現(特指HotSpot),是虛擬機器自身的一部分。負責將存放在%JAVA_HOME%\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中,並且是虛擬機器識別的(僅按照檔案名稱識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類載入到虛擬機器記憶體中。
??另一類就是所有其他的類載入器,這些載入器都是由java實現,獨立於虛擬機器外部。
??Extension ClassLoader:這個類即在其有sun.misc.Launcher$ExtClassLoader實現,它負責載入%JAVA_HOME%\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發人員可以直接使用擴充類載入器。
??Application ClassLoader:這個類載入器由sun.misc.Launcher$AppClassLoader實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的傳回值,所以一般也成它為系統類別載入器。它負責載入使用者類路徑上指定的類庫,開發人員可以直接使用這個類載入器,如果應用程式中沒有自訂過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
??AppClassLoader的parent是ExtClassLoader。很多文章在介紹ClassLoader層次的結構時把Bootstrap ClassLoader也列在ExtClassLoader的上一級中,其實Bootstrap ClassLoader並不屬於JVM的類等級層次,因為Bootstrap ClassLoader沒有遵守ClassLoader的載入股則。另外Bootstrap ClassLoader沒有子類,ExtClassLoader的父類也不是Bootstrap ClassLoader,ExtClassLoader並沒有父類,我們在應用中能提取到的頂層父類是ExtClassLoader.
代碼舉例:
ClassLoader cl = Thread.currentThread().getContextClassLoader(); System.out.println(cl.toString()); System.out.println(cl.getParent()); System.out.println(cl.getParent().getParent());
??輸出結果:
[email protected][email protected]null
如何獲得ClassLoaderthis.getClass().getClassLoader();//使用當前類的ClassLoader Thread.currentThread().getContextClassLoader();//使用當前線程的ClassLoader ClassLoader.getSystemClassLoader();//使用系統ClassLoader
??代碼舉例:
public void test() { System.out.println(this.getClass().getClassLoader().toString()); System.out.println(Thread.currentThread().getContextClassLoader()); System.out.println(ClassLoader.getSystemClassLoader()); }
??輸出:
[email protected][email protected][email protected]
[Tips]如何獲得類的class屬性:
1. Class.forName(類路徑全名);
2. this.getClass();
3. 使用.class,比如String.class
4. 對於基礎資料型別 (Elementary Data Type)有:Class c1 = int.class(class只是約定標記,不是成員屬性) 或者Class c2 = Integer.TYPE
??JVM載入class檔案到記憶體有兩種方式:
??1. 隱式載入:所謂的隱式載入就是不通過在代碼裡調用ClassLoader來記載需要的類,而是通過JVM來自動載入需要的類到記憶體的方式。例如,當我們在類中整合或者引用某個類是,JVM在解析當前這個類時發現引用的類不在記憶體中,那麼就會自動將這些類載入到記憶體中。
??2. 顯示載入:相反的顯示載入就是我們在代碼中通過調用ClassLoader類來載入一個類的方式,例如,調用this.getClass.getClassLoader().loadClass()或者Class.forName(),或者我們自己實現的ClassLoader的findClass()方法等。
雙親委派模型
??雙親委派模型要求畜類鼎城的啟動類載入器(Bootstrap ClassLoader)之外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係一般不會以inheritance的關係實現而是都是使用組合Composition關係來複用父類載入器的代碼。
??雙親委派模型不是一個強制性的約束模型,而是java設計者推薦給開發人員的一種類載入器實現方式。在java的世界中大部分的類載入器都遵循這個模型,但也有例外,到目前為止,雙親委派模型主要出現過3此較大的“被破壞”情況,這個稍後再闡述。
??雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
??雙親委託機制的作用是防止系統jar包被本地替換,因為尋找方法過程都是從最底層開始尋找。 因此,一般我們自訂的classloader都需要採用這種機制,我們只需要繼承java.lang.ClassLoader實現findclass即可,如果需要更多控制,自訂的classloader就需要重寫loadClass方法了,比如tomcat的載入過程,這個比較複雜,可以通過其他文檔資料查看相關介紹。
??雙親委派模型對於保證java程式的穩定運作很重要,實現體現在java.lang.ClassLoader的loadClass()方法中,如下:
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()方法的載入步驟為:
??1. 調用 Class c = findLoadedClass(name);來檢查是否已經載入類;
??2. 在父類載入器上調用loadClass方法:
???c = parent.loadClass(name, false);
??如果父類載入器為null,則使用虛擬機器的內建類載入器:
???c = findBootstrapClassOrNull(name);
??3. 在父類載入器無法載入的時候再調用本身的findClass方法來進行類載入:
???c = findClass(name);
每個ClassLoader載入Class的過程是:
1.檢測此Class是否載入過(即在cache中是否有此Class),如果有到8,如果沒有到2;
2.如果parent classloader不存在(沒有parent,那parent一定是bootstrap classloader了),到4;
3.請求parent classloader載入,如果成功到8,不成功到5;
4.請求jvm從bootstrap classloader中載入,如果成功到8;
5.尋找Class檔案(從與此classloader相關的類路徑中尋找)。如果找不到則到7;
6.從檔案中載入Class,到8;
7.拋出ClassNotFoundException;
8.返回Class.
破壞雙親委派模型
??上文提到了到目前為止,雙親委派模型出現過3次較大規模的“被破壞”的情況,這裡詳細闡述一下。
??第一次。發生在雙親委派模型出現之前(jdk1.2發布之前)。由於雙親委派模型在jdk1.2之後才被引入,而類載入器和抽象類別java.lang.ClassLoader則在jdk1.0時代就已經存在,面對已經存在的使用者自訂類載入器的實現代碼,java設計者引入雙親委派模型時不得不做出了一些妥協。曆史已經成為過去,具體的在此不贅述,需要注意的是jdk1.2之後不提倡使用者再去覆蓋loadClass()方法,而應當把自己的類載入邏輯寫到findClass()中,這樣保證符合雙親委派模型的規則。
??第二次。由模型本身的缺陷所導致的,雙親委派模型很好地解決了各個類載入器的基礎類的統一問題。當父類載入器需要請求子類載入器去完成類載入動作,比如JNDI服務:它的代碼由啟動類載入器去載入,但JNDI的目的是對資源進行集中管理和尋找,它需要調用由獨立廠商實現並部署在應用程式的Classpath下的JNDI提供者(SPI)的代碼,但是啟動類載入器不“認識”這些代碼。這是就要用到了線程上下文載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定。
??這裡怎麼又出來一個context classloader呢?它有什麼用呢?我們在建立一個線程Thread的時候,可以為這個線程通過setContextClassLoader方法來指定一個合適的classloader作為這個線程的context classloader,當此線程啟動並執行時候,我們可以通過getContextClassLoader方法來獲得此context classloader,就可以用它來載入我們所需要的Class。預設的是system classloader。利用這個特性,我們可以“打破”classloader委託機制了,父classloader可以獲得當前線程的context classloader,而這個context classloader可以是它的子classloader或者其他的classloader,那麼父classloader就可以從其獲得所需的 Class,這就打破了只能向父classloader請求的限制了。這個機制可以滿足當我們的classpath是在運行時才確定,並由定製的 classloader載入的時候,由system classloader(即在jvm classpath中)載入的class可以通過context classloader獲得定製的classloader並載入入特定的class(通常是抽象類別和介面,定製的classloader中是其實現),例如web應用中的servlet就是用這種機制載入的.
??第三次。 由於使用者對程式動態性的追求而導致的,這裡所說的“動態性”指的是當前一些非常“熱門”的名稱:代碼熱替換、模組熱部署等,類似於滑鼠鍵盤熱拔插。具體的可以看一下OSGi,在OSGi環境下,類載入器不再是雙親委派模型中的樹型結構,而是一種網狀結構。具體的可以翻閱一些相關資料。