文章目錄
- 類加載機制
- 類的生命周期
- 類的加載程序
- 1、加載
- 2、驗證
- 3、準備
- 4、決議
- 5、初始化
- 類的初始化時機
- 類加載器
- 類與類加載器
- 類加載器分類
- 雙親委派模型
- 作業程序
- 原始碼分析
- 雙親委派機制的好處
類加載機制
類的生命周期
一個型別從被加載到虛擬機記憶體中開始,到卸載出記憶體為止,它的整個生命周期將會經歷加載(Loading)、驗證(Verification)、準備(Preparation)、決議(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準備、決議三個部分統稱為連接(Linking),

類的加載程序

1、加載
類加載程序的第一步,Java虛擬機需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進制位元組流,
- 將這個位元組流所代表的靜態存盤結構轉化為方法區的運行時資料結構,
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口,
2、驗證
確保 Class 檔案的位元組流中包含的資訊符合當前虛擬機的要求,并且不會危害虛擬機自身的安全,
主要包括四種驗證:檔案格式驗證、元資料驗證、位元組碼驗證、符號參考驗證
3、準備
準備階段是正式為類變數分配記憶體并設定類變數初始值(零值)的階段,這些變數所使用的記憶體都應當在方法區中進行分配,
-
這時候進行記憶體分配的僅包括類變數( 即靜態變數,被
static關鍵字修飾的變數),而不包括實體變數,實體變數會在物件實體化時隨著物件一塊分配在 Java 堆中, -
從概念上講,類變數所使用的記憶體都應當在 方法區 中進行分配,不過有一點需要注意的是:JDK 7 之前,HotSpot 使用永久代來實作方法區的時候,實作是完全符合這種邏輯概念的, 而在 JDK 7 及之后,HotSpot 已經把原本放在永久代的字串常量池、靜態變數等移動到堆中,這個時候類變數則會隨著 Class 物件一起存放在 Java 堆中,
-
這里所設定的初始值"通常情況"下是資料型別默認的零值(如0、0.0、0L、null、false等),比如我們定義了
public static int value = 123,那么 value 變數在準備階段的初始值就是 0 而不是123(初始化階段才會賦值), -
這里不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯示初始化:比如給 value 變數加上了 final 關鍵字
public static final int value = 123使其成為常量 ,那么準備階段 value 的值就被賦值為 123,

4、決議
決議階段是虛擬機將常量池內的符號參考替換為直接參考的程序,決議動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法句柄和呼叫限定符7類符號參考進行,
5、初始化
類的初始化階段是類加載的最后一個步驟,此時 Java 虛擬機才真正開始執行類中撰寫的 Java 程式代碼,
初始化階段就是執行類構造器<clinit>()方法的程序,<clinit> ()方法是編譯之后自動生成的,
<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態陳述句塊(static{ }塊)中的陳述句合并產生的,編譯器收集的順序是由陳述句在源檔案中出現的順序決定的,即構造器方法中指令按陳述句在源檔案中出現的順序執行,<clinit>()方法與類的建構式(即在虛擬機視角中的實體構造器<init>()方法)不同,它不需要顯示地呼叫父類構造器,Java虛擬機會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢,- 對于
<clinit>()方法的呼叫,虛擬機會自己確保其在多執行緒環境中的安全性,因為<clinit>()方法是帶鎖執行緒安全,所以在多執行緒環境下進行類初始化的話可能會引起死鎖,并且這種死鎖很難被發現,
類的初始化時機
對于初始化階段,虛擬機嚴格規范了有且只有5種情況下,必須對類進行初始化(只有主動去使用類才會初始化類):
- 當遇到
new、getstatic、putstatic或invokestatic這4條直接碼指令時,比如 new 一個類,讀取一個靜態欄位(未被 final 修飾)、或呼叫一個類的靜態方法時,- 當 jvm 執行
new指令時會初始化類,即當程式創建一個類的實體物件, - 當 jvm 執行
getstatic指令時會初始化類,即程式訪問類的靜態變數(不是靜態常量,常量會被加載到運行時常量池), - 當 jvm 執行
putstatic指令時會初始化類,即程式給類的靜態變數賦值, - 當 jvm 執行
invokestatic指令時會初始化類,即程式呼叫類的靜態方法,
- 當 jvm 執行
- 使用
java.lang.reflect包的方法對類進行反射呼叫時如Class.forName("..."),newInstance()等等,如果類沒初始化,需要觸發其初始化, - 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化,
- 當虛擬機啟動時,用戶需要定義一個要執行的主類 (包含
main方法的那個類),虛擬機會先初始化這個類, MethodHandle和VarHandle可以看作是輕量級的反射呼叫機制,而要想使用這 2 個呼叫, 就必須先使用findStaticVarHandle來初始化要呼叫的類,- 當一個介面中定義了JDK8新加入的默認方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實作類發生了初始化,那該介面要在其之前被初始化,
類加載器
類與類加載器
比較兩個類是否 “相等” ,首先需要這兩個類來源于同一個Class檔案,并且使用同一個類加載器進行加載,這是因為每一個類加載器都擁有一個獨立的類名稱空間,
這里的 “相等” ,包括類的 Class 物件的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的回傳結果為 true,也包括使用 instanceof 關鍵字做物件所屬關系判定結果為 true,
類加載器分類
從 Java 虛擬機的角度來講,只存在以下兩種不同的類加載器:
- 啟動類加載器(Bootstrap ClassLoader),使用 C++ 實作,是虛擬機自身的一部分;
- 其它所有的類加載器,使用 Java 實作,獨立存在于虛擬機外部,全都繼承自抽象類 java.lang.ClassLoader,
從 Java 開發人員的角度看,類加載器可以劃分得更細致一些:
- 啟動類加載器(Bootstrap ClassLoader):此類加載器負責加載
%JAVA_HOME%/lib目錄下的 jar 包和類或者被-Xbootclasspath引數指定的路徑中的所有類,(String等核心類別庫) - 擴展類加載器(Extension ClassLoader):主要負責加載
%JRE_HOME%/lib/ext目錄下的 jar 包和類,或被java.ext.dirs系統變數所指定的路徑下的 jar 包, - 應用程式類加載器(Application ClassLoader):由于這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的回傳值,因此一般稱為系統類加載器,它負責加載用戶類路徑(ClassPath)上所指定的類別庫,如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中默認的類加載器,
如果想要自定義加載器,需要繼承
java.lang.ClassLoader類,且為了滿足雙親委派機制,需要指定父加載器為拓展類加載器
雙親委派模型
下圖展示了各種類加載器之間的層次關系,稱為雙親委派模型(Parents Delegation Model),該模型要求除了頂層的啟動類加載器外,其它的類加載器都要有自己的父類加載器,不過這里類加載器之間的父子關系一般不是以繼承關系(Inheritance)來實作的,而是通常使用組合關系(Composition)來復用父加載器的代碼,

作業程序
每一個類都有一個對應它的類加載器,系統中的 ClassLoader 在協同作業的時候會默認使用 雙親委派模型 ,即在類加載的時候,系統會首先判斷當前類是否被加載過,已經被加載的類會直接回傳,否則才會嘗試加載,加載的時候,首先會把該請求委派給父類加載器的 loadClass() 處理,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器 BootstrapClassLoader 中,當父類加載器無法處理這個加載請求時,子加載才會嘗試自己去完成加載,當父類加載器為 null 時,會使用啟動類加載器 BootstrapClassLoader 作為父類加載器,
當一個 .class 檔案要被加載時,不考慮我們自定義類加載器類,首先會在AppClassLoader中檢查是否加載過,如果有那就無需再加載;如果沒有會交到父加載器,然后呼叫父加載器的loadClass方法,父加載器同樣也會先檢查自己是否已經加載過,如果沒有再往上,直到到達BootstrapClassLoader之前,都是在檢查是否加載過,并不會選擇自己去加載,到了根加載器時,才會開始檢查是否能夠加載當前類,能加載就結束,使用當前的加載器;否則就通知子加載器進行加載;子加載器重復該步驟,如果到最底層還不能加載,就拋出例外
ClassNotFoundException
總結:所有的加載請求都會傳送到根加載器去加載,只有當父加載器無法加載時,子類加載器才會去加載
原始碼分析
核心方法為 java.lang.ClassLoader 的 loadClass()
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,檢查請求的類是否已經被加載過了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { // 父加載器不為空,呼叫父加載器loadClass()方法處理
c = parent.loadClass(name, false);
} else { // 父加載器為空,使用啟動類加載器 BootstrapClassLoader 加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父類加載器拋出ClassNotFoundException,說明父類加載器無法完成加載請求
}
if (c == null) {
long t1 = System.nanoTime();
// 在父類加載器無法加載時,再呼叫本身的findClass方法來進行類加載
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
雙親委派機制的好處
雙親委派模型保證了 Java 程式的穩定運行,可以避免類的重復加載(JVM 區分不同類的方式不僅僅根據類名,相同的類檔案被不同的類加載器加載產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改(沙箱安全機制),如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們撰寫一個稱為 java.lang.Object 類的話,那么程式運行的時候,系統就會出現多個不同的 Object 類,
- 避免類的重復加載
- 保證Java核心類別庫的安全
參考資料
《深入理解Java虛擬機 第3版》——周志明
https://github.com/Snailclimb/JavaGuide
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/336340.html
標籤:其他
下一篇:Linux基本命令(一)
