【java虛擬機】java虛擬機的類加載機制

這篇博文主要來總結一下java虛擬機加載一個類的過程,部分參考自《深入理解Java虛擬機》。爲了避免枯燥的解說,爲了讓讀者在讀完本文後能徹底理解類加載的過程,首先來看一段java代碼,我們從一個例子入手:

//ClassLoaderProcess.java文件
class Singleton {
    private static Singleton singleton = new Singleton();
    public static int count_1;
    public static int count_2 = 0;

    static {
        count_1++;
        count_2++;
    }

    private Singleton() {
        count_1++;
        count_2++;
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

public class ClassLoaderProcess {   

    public static void main(String[] args) {
        System.out.println(Singleton.count_1);
        System.out.println(Singleton.count_2);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

  Singleton是個單例模式的類,裏面有兩個靜態變量,在靜態代碼塊中對兩個靜態變量做自增運算,在私有構造方法中,再對這兩個靜態變量做自增運算,最後打印出來的結果是多少呢?先說一下正確答案不是2和2,而是2和1。我們帶着這個問題去分析虛擬機是如何加載一個類的(如果對虛擬機加載類的過程已經很清楚了,就可以不用往下看了~)。看完本文,相信你也會從虛擬機加載類的過程中來分析這段java代碼了。

2. 虛擬機加載類的過程

2.1 類的生命週期

類的加載過程 
  上圖(我已盡力畫的不那麼醜了>_<)表示一個類的生命週期圖。類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、連接(驗證、準備、解析)、初始化、使用和卸載7個階段。其中加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班的開始,而解析階段則不一定:它再某些情況下可以在初始化階段之後再開始,這是爲了支持java語言的運行時綁定。下面來逐個分析一下類加載的各個過程。

2.2 加載

  我們知道,程序要加載到內存中才可以執行,什麼情況下需要開始類加載過程的第一階段:加載呢?java虛擬機規範中並沒有進行強制約定,這點可以交給虛擬機的具體實現來自由把握。 
  在加載階段,java虛擬機需要完成以下3件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在內存中(堆中)生成一個代表這個類的java.lang.Class對象,用來封裝類在方法區裏的數據結構,作爲方法區中這個類的各種數據的訪問入口

  從這三個步驟中可以很明顯的看出,我們可以通過這個Class來獲取類的各種數據,它就像是一面鏡子,可以反射出類的信息來,所以也就明白了在用反射的時候爲什麼要使用Class了。

2.3 驗證

  驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。 
  一般我們都是通過java文件編譯生成的class文件,這是沒有什麼問題的,但是class文件並不一定要求用java源碼編譯而來,可以使用很多其它的途徑,比如用十六進制編輯器直接編寫來產生class文件。虛擬機如果不檢查輸入的字節流,對其完全信任的話,可能就會因爲載入了有害的字節流而導致系統崩潰,所以驗證是虛擬機的一項重要的工作。

2.4 準備

  接下來就是連接的第二步:準備了。準備階段是正式爲類變量分配內存並設置類變量的初始值的階段。這裏有兩個概念要搞清楚:

  1. 類變量:即被static修飾的靜態變量。
  2. 初始值:指的是該數據類型所對應的“零值”

  所以也就是說,準備階段是爲靜態變量分配內存,並且對其初始化爲零值。不包括靜態代碼塊和實例變量,靜態代碼塊在下面的初始化階段執行,實例變量將會在對象實例化的時候隨着對象一起分到到java堆中的。例如:

public static int value = 123;
  • 1

  在準備階段,value的值爲0,並非123!當然咯,如果是boolean型數據,則爲false。零值是針對具體類型來說的。

2.5 解析

  解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,這個符號引用和直接引用有什麼關聯呢?

  1. 符號引用:以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局是無關的,引用的目標不一定已經加載到內存中。
  2. 直接引用:指的是直接指向目標的指針、相對偏移量或者是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,引用的目標必定已經在內存中存在。

2.6 初始化

  初始化是類加載過程的最後一步,在前面的過程中,除了第一步加載階段用戶可以通過指定自定義類加載器參與外,其餘過程完全是由虛擬機自己主導控制的。到了初始化階段,才真正開始執行類中定義的java程序代碼了(或者說是字節碼)。 
  由上面分析可知,在準備階段,靜態變量已經賦過一次值了,只不過是系統要求的初始值而已,而在初始化階段,爲類的靜態變量賦予程序中指定的初始值,還有執行靜態代碼塊中的程序。 
關於類的初始化這個階段,可以再分析的深入一點,剛剛說初始化的階段是爲類的靜態變量賦實際值的階段,我們也可以從另外的一個角度去表達:初始化階段是執行類構造器方法(注意:不是我們平時說的類的構造方法)的過程,構造器方法是<cinit>()方法,它是由編譯器自動收集類中所有的靜態變量的賦值動作和靜態代碼塊中的語句合併產生的,所以也就清楚了,爲啥初始化階段也可以叫做類構造器方法執行的過程。 
  這裏需要注意的是,編譯器收集的順序是由語句在程序中出現的順序所決定的,靜態代碼塊中只能訪問到定義在靜態代碼塊之前的變量,定義在它之後的變量,在前面的靜態代碼塊中可以賦值,但是不能訪問。可以舉個例子:

public class Test {
    static {
        i = 0; //給變量賦值可以正常通過編譯
        System.out.print(i); //但是不能訪問,這句編譯會提示非法向前引用
    }
    static int i = 1;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

  <cinit>()方法與類的構造函數不同,它不需要顯示的調用父類的構造器,虛擬機會保證在子類的<cinit>()方法執行前,父類的<cinit>()方法已經執行完畢,所以在虛擬機中第一個被執行的<cinit>()方法的類肯定是java.lang.Object。 
  由於父類的<cinit>()方法先執行,也就意味着父類中定義的靜態代碼塊要優先於子類的靜態變量賦值操作,看一個例子:

//演示虛擬機<cinit>方法執行的過程
public class CinitMethod {

    static class Parent{
        public static int A = 1;
        static {
            A = 2;
        }       
    }

    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

  這段程序中,在準備階段,先將A賦爲0,B賦爲0,在初始化階段,先執行父類的<cinit>()方法,所以會執行A=1;然後A=2,然後執行子類的<cinit>()方法,執行B=A,所以打印出來是2。 
  虛擬機會保證一個類的<cinit>()方法在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<cinit>()方法,其它線程都需要阻塞等待,直到活動線程執行完該方法。 
  到這裏,一個類的加載過程就算完畢了,類加載的最終產品是位於堆中的Class對象,封裝了類在方法區內的數據結構,並向java程序員提供了訪問方法區內數據結構的接口。所以程序員就可以使用可以使用這個類去獲取與該類相關的信息了。 
  要注意的是,這是類加載完畢了,跟類的對象是沒有關係的,到目前只能使用類的靜態變量和靜態方法,類的對象需要我們去產生的,有了對象才能操作其中的普通成員變量和方法。 
  現在再去看文章開頭的那段java代碼應該很簡單了,

  1. 在準備階段,java虛擬機將Singleton賦爲空,count_1和count_2賦爲0(count_2賦爲0不是程序中賦的0,是int的默認值)。
  2. 在初始化階段,java虛擬機按照順序執行static代碼, 
    首先實例化Singleton,執行構造方法中的代碼,count_1和cout_2變成1; 
    然後按順序執行static代碼,count_1沒有賦值,還是1,count_2被賦值爲0; 
    最後執行靜態代碼塊中的代碼,count_1和count_2各自增1,所以count_1=2,count_2=1。

分析完畢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章