Java類載入器(深磕3)
【正文】Java類載入器( CLassLoader ) 深磕3:
揭祕 ClassLoader抽象基類
本小節目錄
3.1. 類的載入分類:隱式載入和顯示載入
3.2. 載入一個類的五步工作
3.3. 如何獲取類的載入器
3.4 解刨載入器——ClassLoader抽象基類揭祕
3.5. loadClass 關鍵原始碼分析
3.1. 揭祕ClassLoader抽象基類
3.1.1. 類的載入分類:隱式載入和顯示載入
java中類是動態載入的,jvm啟動的時候,並不會一次性載入所有的class檔案,而是根據需要去動態載入。一是加快啟動的速度,二是節約記憶體。如果一次性載入全部jar包的所有class,速度會很慢。
動態載入一個class類,有兩種方式:
(1) implicit隱式載入
即通過例項化才載入的特性來動態載入class。比如:
IPet dog=new Dog();
在新建物件時,如果Dog.class類沒有載入,則JVM 會在背後呼叫當前的AppClassLoader,載入Dog.class,並且初始化。
(2) explicit顯式載入
explicit顯式方式,又分兩種方式:
一是:java.lang.Class的forName()方法。 比如:
Class<?> aClass = Class.forName(className);
二是:java.lang.ClassLoader的loadClass()方法。 比如:
FileClassLoader classLoader = new FileClassLoader(null,baseDir);
Class<?> aClass = classLoader.loadClass(className);
下面有兩個問題:
兩種顯示載入的方式,有何區別呢?
兩種顯示載入的方式,有何聯絡呢?
花開兩朵,各表一枝。 現在先介紹通過第二種ClassLoader顯式載入方法 ,其類的載入過程。然後再進行兩種方式的比對。
在介紹ClassLoader顯式載入前,先回顧一下類的載入過程。
3.1.2. 載入一個類的五步工作
在瘋狂創客圈的《死磕java》工程原始碼中,有一個常用的系統屬性的配置類——SystemConfig 。下面以次為例,展示一下類的載入過程。
原始碼如下:
package com.crazymakercircle.config; ..................... @ConfigFileAnno(file = "/system.properties") public class SystemConfig extends ConfigProperties { static { Logger.info("開始載入配置檔案到SystemConfig"); //依照註解裝載配置項 loadAnnotations(SystemConfig.class); } .................................... @ConfigFieldAnno(proterty = "debug") public static boolean debug; /** * 寵物工廠類的名稱 */ @ConfigFieldAnno(proterty = "pet.factory.class") public static String PET_FACTORY_CLASS; /** * 寵物模組的類路徑 */ @ConfigFieldAnno(proterty = "pet.lib.path") public static String PET_LIB_PATH; }
編譯完成後,這個”.java”檔案經過Java編譯器編譯成拓展名為”.class”檔案——SystemConfig.class,這個”.class”檔案中儲存著Java程式碼經轉換後的虛擬機器指令。
當需要使用這個類時,虛擬機器將會載入它的SystemConfig.class”檔案,並建立對應的class物件,將class檔案載入到虛擬機器的記憶體,這個過程稱為類載入,這個就是類載入的過程。
類載入的過程分為五步:載入、驗證、準備、解析、初始化。
一、載入:
通過一個類的完全限定名稱,查詢此類位元組碼”.class”檔案,讀入記憶體形成位元組碼流。並建立一個Class物件。
二、驗證
檢查位元組流中包含資訊符合虛擬機器要求,不會危害虛擬機器自身安全。主要包括四種驗證,檔案格式驗證,元資料驗證,位元組碼驗證,符號引用驗證。
三、準備
主要的工作是,在方法區分配靜態變數的記憶體,並且進行記憶體的初始化,設定初始值即0。
四、解析
主要將常量池中的符號引用進行翻譯,翻譯為直接引用,也就是在記憶體中的地址。
五、初始化
首先,如果類的存在靜態變數需要進行賦值,在這個階段完成。
其次,如果有static 靜態塊,在這個階段執行。
再次,若該類具有超類,則對其進行初始化。
例項中,通過這一步,完成執行SystemConfig 類的static塊的執行,並且為每一個配置項賦值,因為都是靜態的。
通過這個五步,一個類的全限定名的”.class”檔案,完成了轉換為一個與目標類對應的java.lang.Class物件例項的工作。 實際的工作,遠遠比上面的陳述的負責。為了方便理解和記憶,上面進行了大大的簡化,只是提取出主要的特徵。
以上五步的中間3個步驟,驗證、準備、解析,合起來有一個統稱,叫類的連結。
3.1.3. 如何獲取類的載入器
使用 getClassLoader() 方法,可以取得類的載入器。如果是應用程式的class path下的類,載入器一般為AppClassLoader 型別。當前,並不是絕對的,這個後面講到如何去定製和修改。
檢視一下當前例項的應用程式類所屬的classLoader,程式碼如下:
public static void showCurrentClassLoader() { Logger.info(""); Logger.info("顯示當前類的ClassLoader::"); Logger.info("class=" + ClassLoaderDemo.class.getCanonicalName()); ClassLoader loader = ClassLoaderDemo.class.getClassLoader(); Logger.info("Loader=" + loader.toString()); }
結果如下:
showCurrentClassLoader |> 顯示當前類的ClassLoader:: showCurrentClassLoader |> class=com.crazymakercircle.classLoaderDemo.base.ClassLoaderDemo showCurrentClassLoader |> [email protected]
程式碼如下:
public static void showAppLoader()
{
String className = "com.crazymakercircle.config.SystemConfig";
Class<?> target = null;
try
{
//根據類名 顯示載入載入類
target = Class.forName(className);
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
Logger.info("顯示載入的類的ClassLoader:");
Logger.info("class=" + target.getCanonicalName());
ClassLoader loader = target.getClassLoader();
Logger.info("Loader=" + loader.toString());
}
結果如下:
showAppLoader |> 顯示載入的類的ClassLoader: showAppLoader |> class=com.crazymakercircle.config.SystemConfig showAppLoader |> [email protected]
%java.ext.dirs% 下jar中類的載入器,型別一定為Extention ClassLoader。比方說,DNSNameService 域名服務類是一個典型的 lib\ext 中的dnsns.jar包中的類,我們來看下他的載入器。程式碼如下:
public static void showExtClassLoader() { ClassLoader loader = DNSNameService.class.getClassLoader(); Logger.info(""); Logger.info("%java.ext.dirs% 下類的ClassLoader:"); Logger.info("class=" + DNSNameService.class.getCanonicalName()); Logger.info("Loader=" + loader.toString()); }
結果如下:
showExtClassLoader |> %java.ext.dirs% 下類的ClassLoader: showExtClassLoader |> class=sun.net.spi.nameservice.dns.DNSNameService showExtClassLoader |> [email protected]
%java.home% 下jar中類的載入器,我們可以理所當然的認為,一定是Bootstrap ClassLoader 載入器型別。 比方說,String 類是一個典型的系統屬性%java.home% 目錄下 rt.jar 中的類,我們來看下他的載入器。程式碼如下:
public static void showBootstrapClassLoader() { ClassLoader loader = String.class.getClassLoader(); Logger.info(""); Logger.info("%java.home%下類的ClassLoader:"); Logger.info("class=" + String.class.getCanonicalName()); Logger.info("Loader=" + loader); }
遺憾的時,結果與前面兩個載入器,大不一樣。
結果如下:
showBootstrapClassLoader |> %java.home%下類的ClassLoader: showBootstrapClassLoader |> class=java.lang.String showBootstrapClassLoader |> Loader=null
1.1.4. 解刨載入器——揭祕ClassLoader抽象基類
ClassLoader類是Java 中的 所有ClassLoader 的基類,在java.lang包中。這是一個抽象類。
ClassLoader類,包含了所有類載入器的三個重要組成部分:
(1)載入成功的Class 物件的快取;
(2)類的查詢路徑
(3)loadClass(String name)
ClassLoader載入器將載入成功的Class 物件,加入一個Vector 動態陣列中,避免重複載入。 這個Vector 動態陣列,也就是載入成功的classes的快取。
原始碼如下:
// The classes loaded by this class loader. The only purpose of this table // is to keep the classes from being GC'ed until the loader is GC'ed. private final Vector<Class<?>> classes = new Vector<>();前面提到,每一個載入器,都有一個自己的查詢範圍,是一系列的jar包或者類路徑,稱之為查詢路徑。形象的說,這個查詢路徑,就像是ClassLoader載入類的專屬的地盤。
其原始碼如下:
// The paths searched for libraries private static String usr_paths[]; private static String sys_paths[];講了這麼多,終於到了關鍵點——ClassLoader載入一個類的祕密在哪裡?。
載入類的方法是:
ClassLoader的loadClass(String name)方法,是類載入器中一個比較重要的方法。其作用是,用於載入一個類。
這個方法,比較理想的流程,大致如下面所示:
(1)首先在快取中找,是否已經載入。如果找到就返回。
(2)如果找不到,就去查詢路徑查詢檔案。如果找出類的.class位元組碼,則去完成載入一個類的五步工作,放入自己的快取中,然後返回給呼叫者。
loadClass(String name)方法,充分將前面的 classes 快取和查詢路徑利用起來,將他們串在了一起。
實際上,這僅僅只是一個理想化的結果。
實際的類載入流程,遠遠比以上的假想流程,複雜得多。
直接來看 loadClass 方法的原始碼吧,這樣反而簡單、粗暴、直接。
1.1.5. loadClass 關鍵原始碼分析
loadClass 方法的原始碼如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //在載入器的快取中,按照類名,查詢已經載入好的現成類 Class<?> c = findLoadedClass(name); //如果載入成功,就直接返回了 //如果還沒有找到,尷尬了 if (c == null) { .... try { if (parent != null) { //優先讓父載入器去載入,若父載入器不為空的話 c = parent.loadClass(name, false); } else { //若父載入器為空,則通過Bootstrap Classloader 載入 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { .... } if (c == null) { // 如果父載入器、啟動類載入器,都沒有找到 // 才呼叫findclass,從自己的地盤,類路徑載入 c = findClass(name); .... } } ....... return c; } } }上面的程式碼中,與流程無關的程式碼直接省略了。
概況一下,loadClass的大致流程如下:
1. 執行findLoadedClass(String)去快取檢測這個class是不是已經載入過了。 在載入器的快取中,按照類名,查詢已經載入好的現成類。如果找到,直接返回了。
2. 如果沒有找到,執行parent父載入器的loadClass方法。通過父載入器載入。注意:這裡不是去自己的地盤查詢class檔案,而是優先通過父載入器載入。這點,非常重要。具體原因,後面會講到。
3. 若父載入器為空,則通過Bootstrap Classloader 載入。
4. 如果父載入器、啟動類載入器,都沒有找到,才呼叫findclass,從自己的地盤,自己的類路徑去查詢位元組碼檔案,通過findClass(String)查詢。
抽絲剝繭之後,loadClass 方法的原始碼其實也不過如此,比較簡單。
但是,這裡已經有兩個疑問:
(1)一個載入器的parent是誰?
(2)為什麼優先從parent載入,而不是從自己的地盤載入?
欲知後事如何,請看下回分解。
原始碼:
程式碼工程: classLoaderDemo.zip
下載地址:在瘋狂創客圈QQ群檔案共享。
瘋狂創客圈:如果說Java是一個武林,這裡的聚集一群武痴, 交流程式設計體驗心得
QQ群連結:瘋狂創客圈QQ群
無程式設計不創客,無案例不學習。 一定記得去跑一跑案例哦
類載入器 全目錄
5. 入門案例:自定義一個檔案系統的自定義classLoader