1. 程式人生 > >深入瞭解類的載入機制

深入瞭解類的載入機制

1.什麼是類的載入

    類的載入指的是將類的.class檔案的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.class物件,用來封裝類在方法區內的資料結構。

2.類的載入過程

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

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


載入:

在載入階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機器需要完成以下3件事情:

  1. 通過一個類的全限定名來獲取其定義的二進位制位元組流。
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
  3. 在Java堆中生成一個代表這個類的java.lang.Class物件,作為對方法區中這些資料的訪問入口。

相對於類載入的其他階段而言,載入階段(準確地說,是載入階段獲取類的二進位制位元組流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類載入器來完成載入,也可以自定義自己的類載入器來完成載入。

載入階段完成後,虛擬機器外部的 二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,而且在Java堆中也建立一個java.lang.Class類的物件,這樣便可以通過該物件訪問方法區中的這些資料。

驗證:

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

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

驗證階段是非常重要的,但不是必須的,它對程式執行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。

準備:

為類的靜態變數分配記憶體,並將其初始化為預設值

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中分配。對於該階段有以下幾點需要注意:

1、這時候進行記憶體分配的僅包括類變數(static),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在Java堆中。

2、這裡所設定的初始值通常情況下是資料型別預設的零值(如0、0L、null、false等),而不是被在Java程式碼中被顯式地賦予的值。

假設一個類變數的定義為:public static int value = 3;

那麼變數value在準備階段過後的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value賦值為3的putstatic指令是在程式編譯後,存放於類構造器<clinit>()方法之中的,所以把value賦值為3的動作將在初始化階段才會執行。

這裡還需要注意如下幾點:

  • · 對基本資料型別來說,對於類變數(static)和全域性變數,如果不顯式地對其賦值而直接使用,則系統會為其賦予預設的零值,而對於區域性變數來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
  • · 對於同時被static和final修飾的常量,必須在宣告的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在宣告時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予預設零值。
  • · 對於引用資料型別reference來說,如陣列引用、物件引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予預設的零值,即null。
  • · 如果在陣列初始化時沒有對陣列中的各元素賦值,那麼其中的元素將根據對應的資料型別而被賦予預設的零值。

3、如果類欄位的欄位屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變數value就會被初始化為ConstValue屬性所指定的值。

假設上面的類變數value被定義為: public static final int value = 3;

編譯時Javac將會為value生成ConstantValue屬性,在準備階段虛擬機器就會根據ConstantValue的設定將value賦值為3。我們可以理解為static final常量在編譯期就將其結果放入了呼叫它的類的常量池中。

解析:

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。

初始化:

初始化,為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化。在Java中對類變數進行初始值設定有兩種方式:

①宣告類變數是指定初始值

②使用靜態程式碼塊為類變數指定初始值

JVM初始化步驟

1、假如這個類還沒有被載入和連線,則程式先載入並連線該類

2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類

3、假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機:只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:

– 建立類的例項,也就是new的方式

– 訪問某個類或介面的靜態變數,或者對該靜態變數賦值

– 呼叫類的靜態方法

– 反射(如Class.forName(“com.shengsiyuan.Test”))

– 初始化某個類的子類,則其父類也會被初始化

– Java虛擬機器啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來執行某個主類

結束生命週期

•在如下幾種情況下,Java虛擬機器將結束生命週期

– 執行了System.exit()方法

– 程式正常執行結束

– 程式在執行過程中遇到了異常或錯誤而異常終止

– 由於作業系統出現錯誤而導致Java虛擬機器程序終止

3.類的載入器

尋找類載入器,例子:

package com.neo.classloader;
public class ClassLoaderTest {
     public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}

輸出結果為:

從上面的結果可以看出,並沒有獲取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引導類載入器)是用C語言實現的,找不到一個確定的返回父Loader的方式,於是就返回null。

站在Java開發人員的角度來看,類載入器可以大致劃分為以下三類:

啟動類載入器:Bootstrap ClassLoader,負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader載入)。啟動類載入器是無法被Java程式直接引用的。

擴充套件類載入器:Extension ClassLoader,該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入DK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充套件類載入器。

應用程式類載入器:Application ClassLoader,該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

應用程式都是由這三種類載入器互相配合進行載入的,如果有必要,我們還可以加入自定義的類載入器。因為JVM自帶的ClassLoader只是懂得從本地檔案系統載入標準的java class檔案,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:

1)在執行非置信程式碼之前,自動驗證數字簽名。

2)動態地建立符合使用者特定需要的定製化構建類。

3)從特定的場所取得java class,例如資料庫中和網路中。

4.類的載入

類載入有三種方式:

1、命令列啟動應用時候由JVM初始化載入

2、通過Class.forName()方法動態載入

3、通過ClassLoader.loadClass()方法動態載入

5.類的載入順序

父類靜態程式碼塊

子類靜態程式碼塊 

父類非靜態程式碼塊
父類構造方法
子類非靜態程式碼塊

子類建構函式
舉例:

public class ExA {  
   static {  
       System.out.println("父類靜態程式碼塊。");  
   }  
   public ExA() {  
       System.out.println("父類建構函式。");  
   }  
   {  
       System.out.println("父類非靜態程式碼塊。");  
   }  
   public static void main(String[] args) {  
       new ExB();  
   }  
}  
class ExB extends ExA {  
   static {  
       System.out.println("子類靜態程式碼塊。");  
   }  
   {  
       System.out.println("子類非靜態程式碼塊。");  
   }  
   public ExB() {  
       System.out.println("子類建構函式。");  
   }  
}  
執行結果:父類靜態程式碼塊。子類靜態程式碼塊。父類非靜態程式碼塊。父類建構函式。子類非靜態程式碼塊。子類建構函式。
參考:https://blog.csdn.net/u013256816/article/details/50829596