概述
如篇首圖所示,Class 檔案被 JVM 載入至 JVM記憶體,在記憶體中驗證、解析、初始化之後,形成可以被 JVM 直接使用的 Java型別。這就是類載入的簡要過程。類的載入過程是在 Java程式執行期間完成,雖然會損耗一部分效能,但是提高了Java語言的靈活性,體現在動態擴充套件方面,例如:多型(晚期繫結)。
類載入的時機
類的生命週期
類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括:
- 載入
- 驗證
- 準備
- 解析
- 初始化
- 使用
- 解除安裝
其中,驗證、準備和解析三個部分稱為連線。解析和初始化的相對順序不是固定的,當解析在初始化之後執行時,稱為動態繫結或者晚期繫結,例如:晚期繫結的多型特性。
初始化
在 JVM 規範中沒有強制約束載入的時機,不過對於初始化,JVM有嚴格的規範,根據《深刻理解JVM虛擬機器》所述,有且只有5種情況必須對類進行初始化,但是我只能理解其中3種:
- 遇到 new、getstatic、putstatic或invokestatic 這4條指令時
如果類沒有進行過初始化,則需要先觸發其初始化。getstatic指讀取一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外);invokestatic指呼叫一個類的靜態方法。 - 使用 java.lang.reflect 包的方法對類進行反射呼叫的時候
如果類沒有進行過初始化,則需要先觸發其初始化。 - 派生類
初始化一個派生類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類
上述三種情況,被稱為對一個類進行 主動引用。除此之外的引用類被稱為 被動引用,不會觸發初始化。
- 通過子類引用父類的靜態欄位,父類初始化,但子類不會初始化
````
package com.zhoupq.jvm.calssload;
public class SuperClass
{
static
{
System.out.println("SuperClass init!");
}
public static int i = 1;複製程式碼
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init!");
}複製程式碼
}
public class TestApp
{
public static void main(String[] args)
{
System.out.println(SubClass.i);
}複製程式碼
}
/
SuperClass init!
1
/
````
類載入的過程
載入
在家在階段,虛擬機器需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流
可以從一個java檔案、jsp檔案獲取class檔案,即生成一個對應的class類。 - 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為這個方法區這個類的各種資料的訪問入口
載入階段與連線階段的部分內容是交叉進行的,載入階段尚未完成,連線階段可能已經開始。
驗證
驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案中的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
準備
準備階段是正式為類變數(被static修飾的變數)分配記憶體並設定類變數初始值(0值)的階段,這些變數所使用的記憶體都將在方法區中進行分配。
方法區中分配的是靜態變數的記憶體,併為其設定0值,具體的值將在初始化階段後再賦值。成員變數(例項變數)隨物件一起在java堆中分配記憶體,具體的值也是在初始化階段後再賦值。
但是如果靜態變數被 final 修飾,那麼該靜態變數在準備階段就會被賦實際的值。
解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。在 JVM(三)——物件的訪問定位 中有介紹什麼是直接引用。
初始化
初始化階段是類載入過程的最後一步,在此之前的階段,基本都是由迅疾主導和控制,在此階段,才真正開始執行類中定義的 Java 程式程式碼。
在準備階段,類變數已經賦過一次初始值(0值),在此階段,則根據程式設計師的主觀計劃去初始化類變數和其他資源。
有一種說法:
初始化階段是執行類構造器 < clinit>()方法的過程
- 類構造器< clinit>() 方法 與 例項構造器< init>()方法(構造方法) 不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類的< clinit>() 方法執行前,父類的< clinit>()方法已經執行完畢。因此在虛擬機器中,第一個被執行的< clinit>()方法的類肯定是 java.lang.Object。
- < clinit>() 方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的。如果沒有靜態語句塊,也沒有對類變數的賦值操作,那麼編譯器可以不生成< clinit>()方法。
- 由於父類的< clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數複製操作。
- 介面中不能使用靜態語句塊,但是仍然有變數初始化的賦值操作,因為介面與類一樣,都會生成< clinit>()方法。但介面與類不同的是,執行介面的< clinit>()方法不需要先執行父介面的< clinit>()方法。只有當父介面中定義的變數使用時,父介面才會初始化。實現類與介面不是繼承關係,所以不存在< clinit>()方法執行的先後順序。
- 在多執行緒中,虛擬機器會保證一個類的< clinit>()方法在被正確地加鎖、同步,只會有一個執行緒去執行這個類的< clinit>()方法,其他執行緒都需要阻塞等待,知道活動執行緒執行< clinit>()方法完畢。並且< clinit>()方法只會被執行一次,當其他執行緒被喚醒後,是不會再進入< clinit>()方法。