1. 程式人生 > >深入理解JAVA虛擬機器讀書筆記----虛擬機器類載入機制

深入理解JAVA虛擬機器讀書筆記----虛擬機器類載入機制

概述

虛擬機器類載入機制:虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。

不像C語言,寫好程式碼後,編譯-》連結-》執行;Java語言裡,型別的載入和連線過程是在程式執行期間完成的。

類載入的時機

虛擬機器規範嚴格規定了有且只有五種情況必須對類進行初始化(載入,驗證,準備自動在之前開始)

1、遇到new,getstatic,putstatic,invokestatic這4條位元組碼指令時,如果類沒有進行初始化,則先初始化。這4個位元組碼常見的出現場景是:使用new關鍵字例項化物件的時候,讀取或設定靜態欄位(被final修飾,已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
2、反射呼叫時
3、初始化一個類時,如果其父類還未初始化,則先出發父類初始化。
4、當虛擬機器啟動時,使用者需要指定一個要執行的主類,虛擬機器會先初始化這個主類
5、使用JDK7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic/REF_putStatic/REF_invokeStatic的方法控制代碼,並且這個方法的控制代碼對應的類沒有進行過初始化,則需要先觸發對其的初始化。

這五種情況稱為對類的主動引用,其他情況稱為被動引用

以下時被動引用的幾種情況:
1、對於訪問靜態欄位,只有直接定義這個欄位的類才被初始化,因此通過子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。
2、通過陣列定義引用類,不會觸發此類的初始化。
3、常量在編譯階段會存入呼叫類的常量池,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

類載入的過程

類載入過程

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中準備、驗證、解析3個部分統稱為連線(Linking)。

載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,類的載入過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)。

載入

虛擬機器需要完成以下3件事情:

  • 通過一個類的全限定名來獲取定義此類的二進位制位元組流(並沒有指明要從一個Class檔案中獲取,可以從其他渠道,譬如:網路、動態生成、資料庫等);
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
  • 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口;

驗證

驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

驗證階段大致會完成4個階段的檢驗動作:

  • 檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別。
  • 元資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
  • 位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。
  • 符號引用驗證:確保解析動作能正確執行。

準備

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。

這時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在堆中。其次,這裡所說的初始值“通常情況”下是資料型別的零值,假設一個類變數的定義為:

public static int value=123;

那變數value在準備階段過後的初始值為0而不是123.因為這時候尚未開始執行任何java方法,而把value賦值為123的putstatic指令是程式被編譯後,存放於類構造器()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。
至於“特殊情況”是指:

public static final int value=123;

即當類欄位的欄位屬性是ConstantValue時,會在準備階段初始化為指定的值,所以標註為final之後,value的值在準備階段初始化為123而非0.

解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

初始化

類初始化階段是類載入過程的最後一步,到了初始化階段,才真正開始執行類中定義的java程式程式碼。初始化階段是執行類構造器()方法的過程。

1、<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。

public class Test
{
    static
    {
        i=0;
        System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
    }
    static int i=1;
}

2、<clinit>()方法與例項構造器<init>()方法不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類<init>()方法執行之前,父類的<clinit>()方法方法已經執行完畢。
3、由於父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。
4、<clinit>()方法對於類或者介面來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生產<clinit>()方法。
5、介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成<clinit>()方法。但介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法。
6、虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確的加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有好事很長的操作,就可能造成多個執行緒阻塞,在實際應用中這種阻塞往往是隱藏的。

類載入器

通過一個類的全限定名來獲取描述此類的二進位制流,執行這個動作的程式碼模組成為“類載入器”。

兩個類只有在同一個類載入器載入的前提下才有意思,否則即使兩個類原子相同的Class檔案,只要載入它們的載入器不同,那這兩個類也是不相等的。

類載入器的分類
對於JVM來說,只存在兩種不同的類載入器:啟動類載入器(Bootstrap ClassLoader),使用C++實現,是虛擬機器自身的一部分。另一種是所有其他的類載入器,使用JAVA實現,獨立於JVM,並且全部繼承自抽象類java.lang.ClassLoader。

對於JAVA開發人員來講,會用到下面三種類載入器:

  • 啟動類載入器(Bootstrap ClassLoader),負責將存放在<JAVA+HOME>\lib目錄中的,或者被-Xbootclasspath引數所制定的路徑中的,並且是JVM識別的(僅按照檔名識別,如rt.jar,如果名字不符合,即使放在lib目錄中也不會被載入),載入到虛擬機器記憶體中,啟動類載入器無法被JAVA程式直接引用。
  • 擴充套件類載入器,由sun.misc.Launcher$ExtClassLoader實現,負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
  • 應用程式類載入器(Application ClassLoader),由sun.misc.Launcher$AppClassLoader來實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般稱它為系統類載入器。負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

應用程式都是由這三種類載入器互相配合進行載入的,如果有必要,我們還可以加入自定義的類載入器。因為JVM自帶的ClassLoader只是懂得從本地檔案系統載入標準的java class檔案,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:
1)在執行非置信程式碼之前,自動驗證數字簽名。
2)動態地建立符合使用者特定需要的定製化構建類。
3)從特定的場所取得java class,例如資料庫中和網路中。

雙親委派模型

雙親委派模型.png
這種層次關係稱為類載入器的雙親委派模型。我們把每一層上面的類載入器叫做當前層類載入器的父載入器,當然,它們之間的父子關係並不是通過繼承關係來實現的,而是使用組合關係來複用父載入器中的程式碼。
雙親委派模型的工作流程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。

使用雙親委派模型來組織類載入器之間的關係,有一個很明顯的好處,就是Java類隨著它的類載入器(說白了,就是它所在的目錄)一起具備了一種帶有優先順序的層次關係,這對於保證Java程式的穩定運作很重要。例如,類java.lang.Object類存放在JDK\jre\lib下的rt.jar之中,因此無論是哪個類載入器要載入此類,最終都會委派給啟動類載入器進行載入,這邊保證了Object類在程式中的各種類載入器中都是同一個類。