Java 類載入機制
Java類載入過程
基於 JDK8,另 ==xx== 為高亮操作,高亮 xx,Typora 支援,而掘金不支援)
類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括
- ==載入== Loading
- 連結 Linking
- ==驗證== Verification
- ==準備== Preparation
- ==解析== Resolution
- ==初始化== Initialization
- ==使用== Using
- ==解除安裝== Unloading
載入、驗證、準備、初始化和 解除安裝 這5個階段的順序是確定的,類的載入過程必須按照這種順序按部就班地開始,然後通常互相交叉地混合式進行,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)。
載入
載入的過程中主要做了3件事:
- 通過一個類的全限定名來獲取定義此類的二進位制流
- 將二進位制流所代表的儲存結構轉化為==方法區==的執行時資料結構。
- 在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區的各種資料訪問入口。
對於一個類或者介面
- 如果該類不是陣列類,則使用類載入器載入二進位制表示。
- 如果該類是陣列類,則由Java虛擬機器建立,因為陣列類不具有外部二進位制表示形式。
類載入器種類
有兩種型別的類載入器:
-
由 JVM 提供的 ==bootstrap 類載入器==:用於載入 系統變數 sun.boot.class.path 所代表的路徑下的 class 檔案,頂層父類
-
使用者定義的類載入器(java 類庫中定義的也包括在內)
- ==ExtClassLoader==:sun.misc.Launcher 類中定義,用於載入 系統變數 java.class.path 所代表的路徑下的 class 檔案;父類載入器為 bootstrap
- ==AppClassLoader==:sun.misc.Launcher 類中定義,用於載入 系統變數 java.ext.dirs 所代表的 路徑下 class 檔案;父類載入器為 ExtClassLoader
- 其他,當然你也可以定義其他型別的類載入器
類載入器載入原理
ClassLoader 使用 ==雙親委派模型== 來載入類,每個 ClassLoader 例項都持有父類載入器的引用,虛擬機器內建的 bootstrap 類載入器為頂層父類載入器,沒有父類載入器,但可以作為其它 ClassLoader 例項的父類載入器。當 ClassLoader 例項需要載入某個類時,它會先委派其父類載入器去載入。這個過程是由上至下依次檢查的,首先由最頂層的類載入器Bootstrap ClassLoader試圖載入,如果沒載入到,則把任務轉交給Extension ClassLoader試圖載入,如果也沒載入到,則轉交給App ClassLoader 進行載入,如果它也沒有載入得到的話,則返回給委託的發起者,由它到指定的檔案系統或網路等URL中載入該類。如果它們都沒有載入到這個類時,則丟擲ClassNotFoundException異常。否則將這個找到的類生成一個類的定義,並將它載入到記憶體當中,最後返回這個類在記憶體中的Class例項物件。
雙親委派機制的優點是可以避免類的重複載入,當父類載入了子類就沒必要再載入。另外能夠保證虛擬機器的安全,防止內部實現類被自定義的類替換。
那麼JVM在搜尋類的時候,如何判斷兩個 class 是否相同呢?答案是不僅全類名要相同 ,而且還要由同一個類載入器例項載入。
驗證
主要確保類或介面的二進位制表示在結構上是正確的。
準備
準備工作包括為類或介面建立靜態欄位,並將這些欄位初始化為其預設值,這裡 不需要執行任何 java 程式碼。
例如,對於類或介面中的如下靜態欄位
private static int num = 666;
複製程式碼
在準備階段會為 num 設定預設值 0;在後面的初始化階段才會給 num 賦值 666;
特殊情況:對於同時被 static 和 final 修飾的 欄位,準備階段就會賦值。
解析
解析是將執行常量池中的符號引用 動態確定為具體值的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符7類符號引用進行。
初始化
初始化階段是初始化類變數和其他資源,或者說是執行類構造器<clinit>()方法的過程.
<clinit>()方法是由編譯器自動收集類中的所有==類變數==(static 修飾的變數)的賦值動作和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。
例如:非法向前引用變數示例
static {
i = 2;
System.out.println(i); //illegal forward reference
}
static int i = 4;
複製程式碼
<clinit>()方法與類的建構函式(或者說例項構造器<init>()方法)不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。因此在虛擬機器中第一個被執行的<clinit>()方法的類肯定是 java.lang.Object。介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法,只有當父介面中定義的變數使用時,父介面才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法。
<clinit>()方法是類構造器,在類初始化的時候執行,用於 類中的靜態程式碼塊 和 靜態欄位 初始化的方法,只會執行一次。
<init>()方法是 類例項的構造器,在物件的初始化階段執行,用於非靜態欄位,非靜態程式碼塊,建構函式的初始化,可以執行多次。
類或者介面只能由於以下原因初始化:
- 使用 new 建立新物件,如果引用的類尚未被初始化,則初始化該類;或者從類中獲取 靜態欄位、設定靜態欄位、執行類中的靜態方法時,如果還沒有被初始化,則 ==宣告該欄位或者方法的類或者介面被初始化==
- 第一次呼叫 java.lang.invoke.method handle例項,呼叫了靜態方法,或者 new 的方法的控制程式碼。
- 使用 java.lang.reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
- 當初始化一個類,而該類直接或者間接 實現了 一個 不含有抽象方法,和靜態方法的介面,則需要初始化該介面。
- 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。
牛刀小試1
執行如下程式碼,輸出如何:
public class SSClass
{
static
{
System.out.println("SSClass");
}
}
public class SuperClass extends SSClass
{
static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass()
{
System.out.println("init SuperClass");
}
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init");
}
static int a;
public SubClass()
{
System.out.println("init SubClass");
}
}
public class NotInitialization
{
public static void main(String[] args)
{
System.out.println(SubClass.value);
}
}
複製程式碼
輸出:
SSClass
SuperClass init!
123
複製程式碼
解析:
上面提到了這樣一句話:
==從類中獲取 靜態欄位、設定靜態欄位、執行類中的靜態方法時,如果還沒有被初始化,則 宣告該欄位或者方法的類或者介面被初始化==
所以 SubClass 類並不會被初始化,所以也就不會執行其 靜態程式碼塊;
牛刀小試2
public class StaticTest {
public static void main(String[] args) {
staticFunction();
}
static StaticTest st = new StaticTest();
static {
System.out.println("1");
}
{
System.out.println("2");
}
StaticTest() {
System.out.println("3");
System.out.println("a=" + a + ",b=" + b);
}
public static void staticFunction() {
System.out.println("4");
}
int a = 110;
static int b = 112;
}
複製程式碼
問題:執行上面的程式,輸出結果是什麼?
答案:執行以上程式碼的輸出結果:
2
3
a=110,b=0
1
4
複製程式碼
解析:
-
執行 main 方法,會導致主類 StaticTest 初始化,由於還未載入,先執行載入,主要分析 準備階段 和 初始化階段 的 賦值操作。
-
準備階段 : 為靜態欄位賦初始值,st 設為 null ,b 設為 0;
-
初始化階段:執行 Java 程式碼的類構造器 <clinit>()方法,分別按順序執行如下程式碼:
- static StaticTest st = new StaticTest();new 會 導致 StaticTest 類執行類初始化,執行 <init>()方法,物件的初始化是先初始化成員變數 和 程式碼塊,再載執行構造方法;
- int a = 110;
- System.out.println("2");
- System.out.println("3");
- System.out.println("a=" + a + ",b=" + b);
- static 靜態程式碼塊,System.out.println("1");
- static int b = 112;
- static StaticTest st = new StaticTest();new 會 導致 StaticTest 類執行類初始化,執行 <init>()方法,物件的初始化是先初始化成員變數 和 程式碼塊,再載執行構造方法;
-
呼叫 staticFunction() 方法,System.out.println("4");
稍微修改一下程式碼,去除 以下程式碼,或許就變得正常多了,
static StaticTest st = new StaticTest();
複製程式碼
輸出:
1
4
複製程式碼
減少了 類例項化的步驟。
父類和子類的初始化順序可以簡單用以下幾句話概括:
- 父類的靜態變數賦值
- 自身的靜態變數賦值
- 父類成員變數賦值和父類塊賦值
- 父類建構函式賦值
- 自身成員變數賦值和自身塊賦值
- 自身建構函式賦值
參考:
深入理解Java虛擬機器