java 類的加載機制
類加載器
類的加載是由類加載器完成的,類加載器包括:啟動類加載器(BootStrap)、擴展類加載器(ExtClassLoader)、應用程序類加載器(AppClassLoader)和自定義類加載器(java.lang.ClassLoader的子類)。
啟動類加載器
一般用本地代碼實現,負責加載JVM基礎核心類庫,即 JAVA_HOME\lib 目錄下的類。
擴展類加載器
繼承自啟動類加載器,加載 \lib\ext 下的類,或者被 java.ext.dirs 系統變量指定的類。
應用程序類加載器
繼承自擴展類加載器,加載 ClassPath 中的類,或者系統變量 java.class.path 所指定的目錄中記載類,是用戶自定義加載器的默認父加載器。
繼承自 ClassLoader 類。
為什麽要自定義類加載器
一方面是由於java代碼很容易被反編譯,如果需要對自己的代碼加密的話,可以對編譯後的代碼進行加密,然後再通過實現自己的自定義類加載器進行解密,最後再加載。
另一方面也有可能從非標準的來源加載代碼,比如從網絡來源,那就需要自己實現一個類加載器,從指定源進行加載。
類加載機制
全盤負責
當一個類加載器負責加載某個 Class 時,該 Class 所依賴的和引用的其他 Class 也將由該類加載器負責載入,除非顯式指定另外一個類加載器來載入。
雙親委派模型
如果一個類加載器收到了 Class 加載的請求,它首先不會自己去嘗試加載這個 Class ,而是把請求委托給父加載器去完成,依次向上。因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的 Class 時,即無法完成該加載,子加載器才會嘗試自己去加載該 Class 。
這樣做的好處是:
1. 避免同一個類被多次加載
2. 安全,Java 核心 API 中定義的類不會被隨意替換
3. 每個加載器只能加載自己範圍內的類
緩存機制
所有加載過的 Class 都會被緩存,當程序中需要使用某個 Class 時,類加載器先從緩存區尋找該 Class ,只有當緩存區不存在時,系統才會去讀取該 Class 對應的二進制數據,並將其轉換成 Class 對象,存入緩存區。
這就是為什麽修改了 Class 後,必須重啟JVM,程序的修改才會生效。
類加載器中的四個重要方法
loadClass(String name, boolean resolve)
protected Class<?> loadClass(String name, boolean resolve)
{
synchronized (getClassLoadingLock(name)) {
// 先從緩存查找該class對象,找到就不用重新加載
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) {
// 如果都沒有找到,則通過自定義實現的findClass去查找並加載
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;
}
}
流程:
緩存 -> 父類加載器 -> 沒有父類 -> 啟動類加載器 -> 自己的 findClass() 方法
findClass(String name)
由自己負責加載類的方法。
在自定義類加載器時,需要重寫該方法並編寫加載規則,取得要加載類的字節碼後轉換成流,然後調用defineClass()方法生成類的 Class 對象。
defineClass(byte[] b, int off, int len)
將 byte 字節流解析成 JVM 能夠識別的 Class 對象。
resolveClass(Class??? c)
解析 Class 對象,即將字節碼文件中的符號引用轉換為直接引用。
符號引用與直接引用
符號引用:即一個字符串,但是這個字符串給出了一些能夠唯一性識別一個方法,一個變量,一個類的相關信息。
直接引用:可以理解為一個內存地址,或者一個偏移量。
舉個例子,現在調用方法 hello(),這個方法的地址是 1234567 ,那麽 hello 就是符號引用,1234567 就是直接引用。
類加載過程
類加載分為三個步驟:加載,連接,初始化
加載
根據一個類的全限定名(如 java.lang.String )來讀取該類的二進制字節流,解析成 JVM 能夠識別的 Class 對象。
連接
驗證
確保 Class 文件的字節流中包含信息符合虛擬機要求,不會危害虛擬機的安全。
主要包括四種驗證:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
準備
為類的靜態變量分配內存並且設置初始值,這裏的初始值指的是不同類型的默認值,如 int 默認值為0,引用的默認值為 null。
而 final 修飾的靜態常量,因為 final 在編譯的時候就會分配了,所以此時的值為代碼中設置的值。
註意
類的靜態變量會分配在方法區中,而實例變量是隨著對象一起分配到 Java 堆中。
解析
將常量池內的符號引用替換為直接引用。
初始化
將靜態變量和靜態方法塊按順序從上到下初始化,即為準備階段的靜態變量重新賦值,設置為代碼中指定的值。
執行構造函數。
如果該類具有父類,先初始化父類。
流程圖
子類繼承父類時的執行順序
---------------------
作者:路比船長
來源:CSDN
原文:https://blog.csdn.net/u013534071/article/details/80254247
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
java 類的加載機制