你不得不知道的,詳細完整的對象實例化過程 1、整體流程 2、類初始化檢查 3、類加載過程 4、分配內存 5、初始化零值 6、設置對象頭 7、實例對象初始化 8、創建引用,入棧

對象的實例化過程需要做哪些工作呢?首先Java是一門面向對象的語言,類是對所屬於一類的所有對象的抽象,對象的所有結構化信息都定義在了類中,因此對象的創建需要根據類中定義的類型信息,也就是類所對應的class二進制字節流,所以這就涉及到了類的加載與初始化。其次,對象大多存儲在堆內存中,這就涉及到內存的分配。除此之外,還有變量的初始化零值,對象頭的設置,在棧中創建對象的引用等等,本文我們來一起詳細的分析一下對象的完整實例化過程。

1、整體流程

從整天上來看對象的整個實例化過程如下圖所示:

爲了故事的順利發展,這裏我們定義一個Demo,並據此詳細討論一下dc對象是如何創建並實例化出來的。

public class Demo
{

    public static void main(String[] args)
    {
        DemoClass dc=new DemoClass();
    }
}
class DemoClass
{
    private static final int a=1;
    private static int b=2;
    private static int c;
    private int d=4;
    private int e;
    static
    {
        c=3;
    }
    public DemoClass()
    {
        e=5;
    }

}

2、類初始化檢查

這裏我們使用new 關鍵字創建對象,Java中創建對象的方式還有好多種,比如反射,克隆,序列化與反序列化等等。這些方式不一而同,但是經過編譯器編譯之後,對應到Java虛擬機中其實就是一條new(這裏的new指令與前面提到的new關鍵字不同,這是虛擬機級別的指令)指令。當Java虛擬機碰到一條new指令時,會首先根據這條指令所對應的參數去常量池中查找是否有該類所對應的符號引用,並判斷該類是否已經被加載、解析、初始化過,也就是到方法區中檢查是否有該類的類型信息,如果沒有,首先要進行類加載與初始化。如果類已經加載和初始化,那麼繼續後續的操作。

​ 這裏假設DemoClass類還沒有被加載與初始化,也就是方法區中還沒有DemoClass的類型信息,這時需要進行DemoClass類的加載與初始化。

3、類加載過程

類加載過程總的可分爲7個步驟:加載、驗證、準備、解析、初始化、使用、卸載。這裏我們看一下前六個階段。

加載

加載階段主要乾了三件事:

  1. 根據類的全限定名獲取類的二進制字節流。
  2. 將二進制字節流所代表的靜態存儲結構轉化爲方法區中運行時數據結構。
  3. 在內存中創建一個代表該類的Java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。

​ 具體到這裏就是首先根據package.DemoClass全限定名定位DemoClass.class二進制文件,然後將該.class文件加載到內存進行解析,將解析之後的結果存儲在方法區中,最後在堆內存中創建一個Java.lang.Class的對象,用來訪問方法區中加載的這些類信息。

驗證

​ 驗證階段完成的任務主要是確保class文件中字節流中包含的信息符合Java虛擬機的規範,雖然說得很簡單,但是Java虛擬機進行了很多複雜的驗證工作,總的來說可分爲四個方面:

  • 文件格式驗證* 元數據驗證* 字節碼驗證* 符號引用驗證具體到這裏就是對於加載進內存的DemoClass.class中存儲的信息進行虛擬機級別的校驗,以確保DemoClass.class中存儲的信息不會危害到Java虛擬機的運行。

準備

​ 準備階段完成的工作就是爲類變量(也就是靜態變量)分配內存並賦予初始值,通常情況下是變量所對應的數據類型的零值。但是在這個階段,被final修飾的變量也就是常量會在這個階段準確的被賦值。

​ 具體到這裏,在這個階段DemoClass中的a會被賦值爲1,b與c均被賦值爲0。

解析

這個階段主要的任務是將常量池中的符號引用替換爲直接引用。

初始化

​ 在之前的階段中,除了加載階段通過自定義的類加載器可以干預虛擬機的加載過程外,其他的階段都是虛擬機完全主導,而在初始化階段纔開始根據程序員的意願執行類的初始化,這個階段主要完成的工作是執行類構造器方法(),同時虛擬機會保證執行該類的類構造器方法時,其父類的類構造器方法已經被正確的執行,同時,由於類的初始化只進行一次,當多個線程併發的進行初始化時,虛擬機可以確保多個線程只有一個可以完成類的初始化工作, 保證線程安全工作。

具體到DemoClass類,在這個階段會將b賦值爲2,c賦值爲3。

4、分配內存

當類加載過程完成後,或者類本身之前已經被加載過,下一步就是虛擬機要爲新生對象分配內存。對象所需要的內存空間在類加載過程完成後就可以完全確定下來,爲對象分配內存空間就相當於從堆內存中劃分出一塊合適的內存來,分配內存的主要方式有兩種:指針碰撞和空閒列表。

  • 指針碰撞:這種方式將堆內存分爲空閒空間與已分配空間,使用一個指針來作爲二者之間的分界線,當要爲新生對象分配內存空間的時候,相當於將指針向着空閒空間的方向移動一段與對象大小相等的距離,可見這種分配方式Java堆內存必須是規整的,所有空閒空間在一邊,已分配空間在另外一邊。
  • 空閒列表:在虛擬機中維護一個列表,用來記錄堆中哪一塊內存是空閒可用的,在爲新生對象分配內存時,從列表中尋找一塊合適大小的可用內存塊,分配完成後更新空閒列表,這種方式下堆內存的空閒空間與分配空間可以交錯存在。

從上面來看,選擇採用指針碰撞還是空閒列表法分配內存,主要由Java堆內存是否規整決定的,而Java堆內存是否規整又取決於所採用的垃圾收集算法,這就涉及到垃圾回收機制(可見知識都是相通的,程序員就是活到老學到死啊!),GC之後是否具有壓縮或者整理的動作等等。

同時,由於創建對象的動作是十分頻繁的,多線程可能存在多個線程同時申請爲對象分配內存空間,這個時候如果不採取一定的同步機制,就有可能導致一個線程還未來得及修改指針,另一個線程就使用了原來的指針分配內存空間,因此衍生出來了兩種解決方案:CAS配上失敗重試、TLAB方式。

第一種方式很好理解,多個線程使用CAS的方式更新指針,多線程下只有一個線程可以更新完成,其他線程通過不斷重試完成內存指針的重新移動。

第二種方式是每個線程提前分配一塊內存空間,這個內存空間就是線程本地緩衝TLAB,這樣線程每次要分配內存時,先去TLAB中獲取,當TLAB中內存空間不足的時候才採用同步機制繼續申請一塊TLAB空間,這樣就降低了同步鎖的申請次數。

具體到這個階段,是在堆內存中爲DemoClass對象,也就是dc對象實例開闢了一塊內存空間。

5、初始化零值

在爲對象分配內存完成之後,虛擬機會將分配到的這塊內存初始化爲零值,這樣也就使得Java中的對象的實例變量可以在不賦初值的情況下使用,因爲代碼所訪問當的就是虛擬機爲這塊內存分配的零值。

具體到這裏,就是Java虛擬機將上面分配的內存空間初始化爲零值,這一步使得現在DemoClass中的d與e均被賦值爲0。

6、設置對象頭

對象頭就像我們人的身份證一樣,存放了一些標識對象的數據,也就是對象的一些元數據,我們首先看一下對象的構成。

在初始化了零值之後,怎麼知道對象是哪個類的實例,就需要設置指向方法區中類型信息的指針,對象Mark Word中相關信息的設置,就在這個階段完成。

7、實例對象初始化

這一步虛擬機將調用實例構造器方法(), 根據我們程序員的意願初始化對象,在這一步會調用構造函數,完成實例對象的初始化。

具體到這裏就是DemoClass的d被賦值爲4,e被賦值爲5。

8、創建引用,入棧

執行到這一步,堆內存中已經存在被完成創建完成的對象,但是我們知道,在Java中使用對象是通過虛擬機棧中的引用來獲取對象屬性,調用對象的方法,因此這一步將創建對象的引用,並壓如虛擬機棧中,最終返回引用供我們使用。

在這裏就是講對象的引入入棧,並返回賦值給dc,至此,一個對象被創建完成。

對象實例化的完整流程

根據上面的討論,我們再來回顧一下對象實例化的整個流程:

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