4.1 java 類加載器
一,類的加載
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個這個類的Java.lang.Class對象,用來封裝類在方法區類的對象。看下面2圖
類的加載的最終產品是位於堆區中的Class對象 Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口
加載類的方式有以下幾種:
- 從本地系統直接加載
- 通過網絡下載.class文件
- 從zip,jar等歸檔文件中加載.class文件
- 從專有數據庫中提取.class文件
- 將Java源文件動態編譯為.class文件(服務器)
二,加載步驟
JVM將類加載過程分為三個步驟:裝載(Load),鏈接(Link)和初始化(Initialize)鏈接又分為三個步驟,如下圖所示:
1. 裝載:查找並加載類的二進制數據;
2. 鏈接:
- 驗證:確保被加載類的正確性
那為什麽我要有驗證這一步驟呢? 首先如果由編譯器生成的class文件,它肯定是符合JVM字節碼格式的,但是萬一有高手自己寫一個class文件,讓JVM加載並運行,用於惡意用途,就不妙了,因此這個class文件要先過驗證這一關,不符合的話不會讓它繼續執行的,也是為了安全考慮吧
-
準備:為類的靜態變量分配內存,並將其初始化為默認值
-
解析:把類中的符號引用轉換為直接引用
3. 初始化:為類的靜態變量賦予正確的初始值
準備階段和初始化階段看似有點矛盾,其實是不矛盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先字節碼文件被加載到內存後,先進行鏈接的驗證這一步驟,驗證通過後準備階段,給a分配內存,因為變量a是static的,所以此時a等於int類型的默認初始值0,即a=0,然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10
對於
1. 靜態變量
2. 靜態初始化塊
3. 變量
4. 初始化塊
5. 構造器
-
它們的初始化順序依次是(靜態變量、靜態初始化塊)>(變量、初始化塊)>構造器
-
靜態變量和靜態初始化塊是依照他們在類中的定義順序進行初始化的。同樣,變量和初始化塊也遵循這個規律。
-
並不是父類完全初始化完畢後才進行子類的初始化,實際上子類的靜態變量和靜態初始化塊的初始化是在父類的變量、初始化塊和構造器初始化之前就完成了
三,加載器
JVM的類加載是通過ClassLoader及其子類來完成的
類的層次關系和加載順序可以由下圖來描述:
驗證ClassLoader加載類的原理:
測試1:打印ClassLoader類的層次結構,請看下面這段代碼:
ClassLoader loader = ClassLoaderTest.class.getClassLoader(); //獲得加載ClassLoaderTest.class這個類的類加載器
while(loader != null) {
System.out.println(loader);
loader = loader.getParent(); //獲得父類加載器的引用
}
System.out.println(loader);
1. Bootstrap ClassLoader
根加載器,由C++實現,不是ClassLoader子類.是Java類加載層次中最頂層的類加載器,負責加載JDK中的核心類庫,如rt.jar、resources.jar、charsets.jar等,可通過如下程序獲得該類加載器從哪些地方加載了相關的jar或class文件:
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
2. Extension ClassLoader
擴展類加載器,負責加載Java的擴展類庫,默認加載JAVA_HOME/jre/lib/ext/目下的所有jar。
3.App ClassLoader/System ClassLoader
應用類加載器/系統類加載器,負責記載classpath中指定的jar包及目錄中class
4.Custom ClassLoader
屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader
5.加載原理
ClassLoader使用的是雙親委托模型來搜索類的
-
每個ClassLoader實例都有一個父類加載器的引用(不是繼承的關系,是一個包含的關系)
-
虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它ClassLoader實例的的父類加載器
-
當一個ClassLoader實例需要加載某個類時,它會試圖親自搜索某個類之前,先把這個任務委托給它的父類加載器,
這個過程是由上至下依次檢查的,
首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,
如果沒加載到,則把任務轉交給Extension ClassLoader試圖加載,
如果也沒加載到,則轉交給App ClassLoader 進行加載,
如果它也沒有加載得到的話,則返回給委托的發起者,由它到指定的文件系統或網絡等URL中加載該類。
如果它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。
否則將這個找到的類生成一個類的定義,並將它加載到內存當中,最後返回這個類在內存中的Class實例對象。
6.全盤負責
“全盤負責”是指當一個ClassLoader裝載一個類時,除非顯示地使用另一個ClassLoader,則該類所依賴及引用的類也由這個CladdLoader載入。
7.雙親委托
雙親委托可以避免重復加載,
當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。
考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在非常大的安全隱患,
而雙親委托的方式,就可以避免這種情況,
因為String已經在啟動時就被引導類加載器(Bootstrcp ClassLoader)加載,
所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,
除非你改變JDK中ClassLoader搜索類的默認算法。
8.JVM 判定類相同
JVM在判定兩個class是否相同時,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的。
只有兩者同時滿足的情況下,JVM才認為這兩個class是相同的。
四,一個類的字節碼僅被加載一次
- ExtClassLoader如何保證字節碼僅被加載一次
邏輯上來說ExtClassLoader的父加載器是Bootstrap,
具體到代碼,ExtClassLoader繼承了URLClassLoader,URLClassLoader繼承了類SecureClassLoader,最終一直到抽象類ClassLoader。
為了保證“一個類的字節碼僅被加載一次”這個目標。ExtClassLoader要做的有兩件事:
a.保證“ private ClassLoader parent;”正確。(對於ExtClassLoader來說這裏為null)
b.不要重寫“ public Class<?> loadClass(*) throws ClassNotFoundException”方法。
而最終頂級抽象類ClassLoader中是這麽寫的
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通過上面的代碼可以看出來,實際加載類時,子加載器遞歸調用父加載器的loadClass(**)方法,直到到達頂級(parent==null)時,方才調用findBootstrapClass0(**)。這時如果拋出了ClassNotFoundException異常,就表示還沒加載呢,然後去調用具體的 findClass(name)實現加載。
- AppClassLoader 如何保證字節碼僅被加載一次
AppClassLoader與ExtClassLoader在保證“一個類的字節碼僅被加載一次”這個目標上類似。
雖然loadClass方法,但在最後調用了“return super.loadClass(paramString, paramBoolean);”,所以仍舊是OK的。
如下所示:
public synchronized Class loadClass(String paramString, boolean paramBoolean)
throws ClassNotFoundException
{
DownloadManager.getBootClassPathEntryForClass(paramString);
int i = paramString.lastIndexOf(46);
if (i != -1) {
SecurityManager localSecurityManager = System.getSecurityManager();
if (localSecurityManager != null)
localSecurityManager.checkPackageAccess(paramString.substring(0, i));
}
return super.loadClass(paramString, paramBoolean);
}
五,自定義自己的ClassLoader
既然JVM已經提供了默認的類加載器,為什麽還要定義自已的類加載器呢?
因為Java中提供的默認ClassLoader,只加載指定目錄下的jar和class,如果我們想加載其它位置的類或jar時,
比如:我要加載網絡上的一個class文件,通過動態加載到內存之後,要調用這個類中的方法實現我的業務邏輯。
在這樣的情況下,默認的ClassLoader就不能滿足我們的需求了,所以需要定義自己的ClassLoader。
定義自已的類加載器分為兩步
自定義ClassLoader需要繼承ClassLoader抽象類,重寫findClass方法,這個方法定義了ClassLoader查找class的方式。
- 繼承java.lang.ClassLoader
如果嫌麻煩的話,我們可以直接使用或繼承已有的ClassLoader實現,比如
java.net.URLClassLoader
java.security.SecureClassLoader
java.rmi.server.RMIClassLoader
sun.applet.AppletClassLoader
Extension ClassLoader 和 App ClassLoader都是java.net.URLClassLoader的子類。
這個是URLClassLoader的構造方法:
public URLClassLoader(URL[] urls, ClassLoader parent)
public URLClassLoader(URL[] urls)
- 重寫父類的findClass方法
如果想保持雙親委派模型,就應該重寫findClass(name)方法;如果想破壞雙親委派模型,可以重寫loadClass(name)方法。
- findClass:定義查找Class的方式
- defineClass:將類文件字節碼加載為jvm中的class
- findResource:定義查找資源的方式
4.1 java 類加載器