Java源碼解讀篇之ClassLoader
我們知道任何程序都需要加載到內存中才能與CPU 進行交流。
字節碼.class 文件同樣需要加載到內存中,纔可以實例化類。
而將.class 文件加載到內存中的就是ClassLoader.
ClassLoader 是一個類加載器,主要用於將*.class 信息加載到JVM 的不同區域中.
但是這都是我們道聽途說對它的瞭解,實際上真實的它到底是怎麼樣的呢?
它是怎麼查找和加載*.class 的呢?
今天我們就一起嘗試閱讀下這個ClassLoader 源碼,來從源碼層面認識下它.
1.1 什麼是Class Loader?
- Java 世界中,萬事萬物皆爲對象,一個類加載器(Class Loader)也是一個對象,它主要負責加載 classes.
- 類加載是一個將.class 字節碼文件實例化成Class 對象並進行相關初始化的過程。
- 值得注意的是通過查看源碼我們可以看到這個ClassLoader類其實是一個抽象類,而不是印象中的普通類.
public abstract class ClassLoader {
...
}
1.2 Java類加載之雙親委派模型
提起Classs Loader 的類加載機制就不得不說說它的“雙親委派模型“,當然叫“溯源委派加載模型”更加貼切。
1.2.1 什麼是雙親委派模型?
那麼什麼是“雙親委派模型”呢?
一圖勝千言
如上圖所示,類加載器是存在嚴格的等級制度,一層一重天。
- 最高一層是BootStrap Class Loader
它是在JVM 啓動時候創建的,負載裝載核心的Java 類, 比如Object , System String 等,主要位於jre/lib/rt.jar
中。- 第二層是Platform ClassLoader
即平臺類加載器,負責裝載擴展系統類,比如XMl, 加密,壓縮相關的功能類,主要位於jre/lib/ext/*.jar
- 第三層類是Application ClassLoader
即應用類加載器,主要加載用戶自定義的ClassPath 路徑下的類,主要位於ClassPath 中的jar.
1.2.2 爲什麼叫雙親委派模型?
- 距離探索的真相似乎又近了一步,那麼爲什麼叫雙親委派模型呢?
這是因爲如果我們的Application ClassLoader 如果想加載一個未知的類,它需要問下Platform ClassLoader和BootStrap ClassLoader的意見,他們兩個父加載器都沒有加載過這個類,並且告知可以加載它,我們的Application ClassLoader纔可以加載它。
- 多學一招
BootStrap 加載的路徑可以追加但是不建議修改或刪除原有加載路徑,添加如下啓動參數可添加新的加載路徑:
Xbootclasspath/a:/Users/administrator/myProject/src/main/java
如果想啓動時觀察加載了哪個jar 包中的哪個類,可以添加如下啓動參數:
-XX:+TraceClassLoading
1.3 Class Loader 類加載器之如何加載字節碼
1.3.1 Class Loader 如何加載本地字節碼?
一個Class Loader 查找過程是究竟是怎麼樣的呢?
我們知道每個類都有自己的類加載器,但是如此同時,又和父類的類加載器存在那麼一絲聯繫.
當一個類開始查找class 和resources 的時候,會委託爲它的父類的類加載器去搜索和查找.
ClassLoader 有一個抽象方法如下:
Class<?> loadClass(String name)
其實Class Loader 類加載器不止可以加載本地的classs 還支持加載遠程服務器上 的classes.
1.3.2 Class Loader 如何加載網絡中的字節碼?
如何加載一個遠程的class 呢? 首先需要一個URL地址,其次是端口, 然後是資源名稱路徑.
然後我們需要將其下載到一個路徑下,然後再去加載.
ClassLoader 提供相關的兩個方法如下
URL getResource(String name)
上面方法的含義是:查找具有給定名稱的資源,資源就是一些數據(圖像,音頻,文本等)可以由類代碼以某種方式訪問與代碼的位置無關,路徑分割符號爲
/
URL findResource(String name)
name
:資源的名稱 返回資源所在的URL地址對象- 查找具有給定名稱的資源。 類加載器實現應該重寫此方法以指定在何處查找資源
1.3.3 如何使用並行功能加載器加快加載速度?
上面我們很顯然可以看出Class Loader 的加載需要一個過程,那麼如果想提高它的加載速度.
那麼使用並行功能類加載器似乎是一個不錯的方法.
支持併發加載類的類加載器稱爲並行功能類加載器, 這種加載器類似我們之前學習的併發編程一樣,如果加載的過程是一個漫長的阻塞過程,那麼使用並行無疑可以提高一定的加載速度.
類加載器需要具有並行功能,否則類加載會導致死鎖,因爲在類加載過程loadClass
方法的持續時間內會保持加載器鎖
也就是說一旦我們使用了並行功能類加載器加載class,那麼就可能會發生共享資源的爭搶問題,甚至有可能發生死鎖問題.
計算機的虛擬世界是公平的世界, 給你一個優點的同時就會給你一個缺點.
多線程是把雙刃劍,用好了可以提高效率,用不好就會引入新的問題.
- 給出一個類的二進制名稱,類加載器(class Loader)會嘗試查找或生成構成類定義的數據。
- 一種比較經典的做法是將類的名稱轉換成一個class 文件,然後從文件系統中讀取這個class文件。
- 每一個對象都包含一個類加載的引用,比如頂級父類具有
Object.class.getClassLoader();
方法
1.4 自定義ClassLoader
那麼你又沒有好奇什麼時候需要自定義類加載器,如果想自定義類加載器又該怎麼做呢?
1.4.1 什麼時候需要自定義類加載器?
當出現如下幾種場景的時候可以考慮使用類加載器。
- 隔離加載類
有一個項目A, 需要引入某一個組件B, 組件B 傳遞依賴必須用組件C 的1.x 版本,而項目A必須用組件C 2.x版本。
- 修改類加載方式
- 擴展加載源
比如需要從數據庫,網絡,甚至電視機頂盒進行加載
- 防止源碼泄漏
java 代碼很容易被反編譯,因此可以進行編譯加密,通過自定義類加載器的方式可以實現這一目的。
1.4.2 如何自定義一個類加載器?
那麼如果我們想自定義一個類加載器該怎麼做呢?
自定義ClassLoader 的方法很簡單,繼承自抽象類ClassLoader 即可。
public class CustomizeClassLoader extends ClassLoader {
}
1.4.3 ClassLoader支持重寫覆蓋的方法有哪些?
接下來我們重點看看支持重寫哪些方法
我這裏彙總了一張表格,除了基礎的構造方法之外支持重寫的方法如下:
方法定義 | 參數解釋 | 方法解釋 |
---|---|---|
Class<?> loadClass(String name) | name指當前類二進制的名稱 | 根據當前類的二進制名稱加載類 |
Class<?> loadClass(String name, boolean resolve) | name指當前類的二進制名稱,resolve 指是否解析該類,如果爲true 則解析該類 | 告訴類加載器是否解析這個類 |
Object getClassLoadingLock(String className) | className指待加載類的名稱,返回類加載操作的鎖 | 回用於類加載操作的鎖對象。 |
Class<?> findClass(String name) | name:類的二進制名稱,返回類的對象 | 查找具有指定的二進制名稱的類 |
URL getResource(String name) | name:資源的名稱 | 查找具有給定名稱的資源,資源就是一些數據(圖像,音頻,文本等)可以由類代碼以某種方式訪問與代碼的位置無關,路徑分割符號爲/ |
Enumeration<URL> getResources(String name) |
name:資源的名稱 | 查找具有給定名稱的所有資源 |
URL findResource(String name) | name:資源的名稱 返回資源所在的URL地址對象 | 查找具有給定名稱的資源。 類加載器實現應該重寫此方法以指定在何處查找資源 |
Enumeration<URL> findResources(String name) |
資源 的 java.net.URL的枚舉URL對象 | |
InputStream getResourceAsStream(String name) | name:資源的名稱 | 用於讀取資源的輸入流,如果找不到資源則爲null |
Package definePackage(String name, String specTitle, String specVersion, String specVendor, String implTitle, String implVersion, String implVendor, URL sealBase) | name:包的名稱; specTitle:規格名稱;specVersion: 規格版本;specVendor 規格供應商 implTitle : 實現標題 implVersion:實現版本;implVendor:實現供應商;sealBase:如果不爲null,則使用關於給定代碼源java.net.URL URL對象。 否則,package將不被密封。 | 返回新定義的Package對象 |
Package getPackage(String name) | package:包的名稱 | 返回指定包名下類加載定義的package |
Package[] getPackages() | 返回這個類加載器下的所有package數組 | |
String findLibrary(String libname) | libname:類庫的名稱,返回本地庫的絕對路徑 | |
void setDefaultAssertionStatus(boolean enabled) | enabled:true 表示如果此類加載器加載的類將此後默認啓用斷言,如果默認情況下將禁用斷言則設置爲false | |
void setPackageAssertionStatus(String packageName, boolean enabled) | packageName:要設置其軟件包默認斷言狀態的軟件包的名稱,如果爲null,則表示未命名的程序包是當前的,enabled:如果此類加載器和屬於指定軟件包或其任何子軟件包的默認情況下啓用斷言,false表示默認情況下禁用斷言 | |
void setClassAssertionStatus(String className, boolean enabled) | className:要設置其聲明狀態的頂級類的標準類名稱。enabled:如果類初始化的時候這個類有斷言,設置爲true,否則設置爲false 禁用斷言 | 此爲命名的頂級類設置所需的斷言狀態.如果命名的類不是頂級類,則此調用將對任何類的實際斷言狀態沒有影響 |
void clearAssertionStatus() | 該方法將此類加載器的默認斷言狀態設置爲false,並丟棄任何程序包默認值或類斷言與類加載器關聯的狀態設置. 這個方法存在的意義是可以使類加載器忽略任何命令行 |
1.5 總結
彙總本篇博文所學內容如下:
1.6 參考資料
- JDK 源碼
- 《碼出高效 Java 開發手冊》