1. 程式人生 > >JVM初探(一)——類的載入、連線和初始化

JVM初探(一)——類的載入、連線和初始化

一、載入

類的載入就是查詢並載入類的二進位制資料。最常見的情況就是將一個已經存在在磁碟的 .class 檔案載入到記憶體中。

類的載入指的是將類的 .class 檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在記憶體中建立一個java.lang.Class物件(JVM規範並未說明Class物件位於哪裡,HotSpot虛擬機器將其放在了方法區中),用來封裝類在方法區內的資料結構。

類的載入的最終產品是位於堆區(HotSpot虛擬機器)中的Class物件。Class物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了訪問方法區內的資料結構的介面。同時,Class物件也是Java反射機制的入口。

載入.class檔案的方式
– 從本地系統中直接載入
– 通過網路下載.class檔案
– 從zipjar等歸檔檔案中載入.class檔案
– 從專有資料庫中提取.class檔案
– 將Java原始檔動態編譯為.class檔案 (例如動態代理;web開發中的jsp轉換為servlet類,然後編譯為.class檔案)

類載入器並不需要等到某個類被首次主動使用”時再載入它 。

JVM規範允許類載入器在預料某個類將要被使用時就預先載入它,如果在預先載入的過程中遇到了 .class 檔案缺失或存在錯誤,類載入器必須在程式首次主動使用該類時才報告錯誤(LinkageError錯誤)。如果這個類一直沒有被程式主動使用,那麼類載入器就不會報告錯誤 。

二、連線

類被載入後,就進入連線階段。連線就是將已經讀入到記憶體的類的二進位制資料合併到虛擬機器的執行時環境中去。連線又分為以下三個階段

驗證

確保被載入的類的正確性。類的驗證主要包括以下內容:

·類檔案的結構檢查:確保類檔案遵從Java類檔案的固定格式

·語義檢查:確保類本身符合Java語言的語法規定,比如驗證final型別的類沒有子類,以及final型別的方法沒有被覆蓋。

·位元組碼驗證:確保位元組碼流可以被Java虛擬機器安全地執行。位元組碼流代表Java方法(包括靜態方法和例項方法),它是由被稱作操作碼的單位元組指令組成的序列,每一個操作碼後都跟著一個或多個運算元。位元組碼驗證步驟會檢查每個操作碼是否合法,即是否有著合法的運算元。

·二進位制相容的驗證:確保相互引用的類之間協調一致。例如在Test1類的test()方法中會呼叫Test2類的run()方法。Java虛擬機器在驗證Test1類時,會檢查在方法區內是否存在Test2類的run()方法,假如不存在(當Test1類和Test2類的版本不相容時,就會出現這種問題),就會丟擲NoSuchMethodError錯誤。

準備

在準備階段,Java虛擬機器為類的靜態變數分配記憶體,並設定預設的初始值。例如對於下面的Test類,在準備階段,將為int型別的靜態變數a分配4個位元組的記憶體空間,並且賦予預設值0,為long型別的靜態變數b分配8個位元組的記憶體空間,並且賦予預設值0

public class Test {

    private static int a = 1;

    private static long b;

    static {

        b = 2;

    }

    ......
  
}

解析

在解析階段,Java虛擬機器會把類二進位制資料中的符號引用轉換為直接引用。例如在Test1類的test()方法中會引用Test2類的run()方法

public void test() {

    test2.tun();  //這句程式碼在Test1類的二進位制資料中表示為符號引用

}

Test1類的二進位制資料中,包含了一個對Test2類的run()方法的符號引用,它由run()方法的全名和相關描述符組成。在解析階段,Java虛擬機器會把這個符號引用替換為一個指標。該指標指向Test2類的run()方法在方法區內的記憶體位置,這個指標就是直接引用。

三、初始化

為類的靜態變數賦予正確的初始值。

在初始化階段,Java虛擬機器執行類的初始化語句,為類的靜態變數賦予初始值。在程式中,靜態變數的初始化有兩種途徑:

(1)在靜態變數的宣告處進行初始化;

(2)在靜態程式碼塊中進行初始化

例如在以下程式碼中,靜態變數ab都被顯式初始化,而靜態變數c沒有顯式初始化,它將保持預設值0

public class Sample {

    private static int a = 1;  //在靜態變數的宣告處進行初始化

    private static long b;

    private static long c;

    static {

        b = 2;  //在靜態程式碼塊中進行初始化

    }

    ......

} 

靜態變數的宣告語句,以及靜態程式碼塊都被看作類的初始化語句,Java虛擬機器會按照初始化語句在類檔案中的先後順序來依次執行它們。例如當以下Sample類被初始化後,它的靜態變數a的取值為4

public class Sample {

    static int a = 1;

    static { a = 2;}

    static { a = 4;}

    public static void main(String args[]) {

        System.out.println(“a=” + a);  //列印a=4

    }

} 

類的初始化步驟:

(1)假如這個類還沒有被載入和連線,那就先進行載入和連線。

(2)假如類存在直接的父類,並且這個父類還沒有被初始化,那就先初始化直接的父類。

(3)假如類中存在初始化語句,那就依次執行這些初始化語句。

Java虛擬機器初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則並不適用於介面:

·在初始化一個類時,並不會先初始化它所實現的介面。

·在初始化一個介面時,並不是先初始化它的父介面。

因此,一個父介面並不會因為它的子介面或者實現類的初始化而初始化。只有當程式首次使用特定介面的靜態變數時,才會導致該介面的初始化。

·只有當程式訪問的靜態變數或靜態方法確實在當前類或當前介面中定義時,才可以認為是對類或介面的主動使用。

·呼叫ClassLoader類的loadClass方法載入一個類,並不是對類的主動使用,不會導致類的初始化。

Java程式對類的使用方式可分為兩種

– 主動使用

– 被動使用

所有的Java虛擬機器實現必須在每個類或介面被Java程式“首次主動使用”時才初始化他們。

主動使用(七種)

i. 建立類的例項,例如執行new object()

ii. 訪問某個類或介面的靜態變數(取值)-getstatic助記符;或者對該靜態變數賦值-putstatic助記符

iii. 呼叫類的靜態方法-invokestatic助記符

iv. 反射,如Class.forName(com.test.Test)

v. 初始化一個類的子類;如Child類繼續Parent類,當初始化一個Child類的時候,Parent類也會被初始化

vi. Java虛擬機器啟動時被標明為啟動類的類;換句話說,包含了main方法的類

vii. JDK1.7開始提供的動態語言支援:java.lang.invoke.MethodHandle例項的解析結果REF_getStaticREF_putStaticREF_invokeStatic控制代碼對應的類沒有初始化則初始化

被動使用:除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化。