java物件建立流程
物件建立流程
建立觸發
關於物件的建立一般是從new指令(我說的是JVM的層面)開始的。 虛擬機器遇到一條new指令時,會先去檢查這個指令的引數能否在方法區中的常量池中檢索到一個類的符號應用,並且檢查這個符號引用代表的類是否已被載入、解析、初始化。如果沒有,則必須先執行相應的類載入過程。(下次會介紹類的載入過程)。
分配記憶體
類載入檢查通過後,接下來JVM開始為物件在堆中分配記憶體。物件所需要的記憶體大小在類載入完成後遍可以完全確定。為物件分配空間的任務就相當於把一塊確認大小的記憶體從java堆中劃分出來。而根據記憶體是否規則化分為兩種情況:
- “指標碰撞方式”:java堆中的記憶體是絕對規整的。所有用過的記憶體都放在一邊,所有未使用過的記憶體放在另一邊。中間放著一個指標作為分界點的指示器。那在這種情況下分配記憶體就相當於指標往空閒方向移動了一段與物件大小相等的距離。
- “空閒列表方式”:java堆中的記憶體不是規整的,已使用記憶體和空閒記憶體相互交錯,這時候就不能簡單的像指標碰撞一樣為物件分配記憶體了,JVM就需要維護一個列表,記錄哪些記憶體是使用過的,哪些是空閒的,在為物件分配記憶體的時候從這個列表中找到一個足夠大的空間劃分給物件,並更新列表上的記錄。
**因此,選擇哪種分配方式實際上是由記憶體是否規整來決定,而記憶體是否規整又由JVM選擇的垃圾收集器是否帶有壓縮整理記憶體功能的有關係。**比如serial、ParNew等使用複製/標記整理的垃圾收集器,就是使用指標碰撞方式。而像CMS這種基於Mark-Sweep(標記-整理)演算法的收集器,則JVM使用空閒列表方式分配記憶體。關於垃圾回收器的知識,我會在後面單獨講。
同時,除了要考慮記憶體分配的方式之外,還需要考慮記憶體分配的併發性,要保證指標的一致性。因為物件在JVM中的建立是非常頻繁的行為,即使是僅僅修改一個指標所指向的位置就能為物件分配記憶體,在併發的情況下也不是執行緒安全的。可能出現正在給物件A分配記憶體,指標還沒來得及修改,JVM又使用原來的指標為物件B分配了記憶體。解決這個問題有兩種方法:
- 對分配記憶體空間的動作進行同步處理——實際上虛擬機器採用了CAS配上失敗重試的方式保證更新操作的原子性。
- 另一種方式是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在java堆中預先分配一塊記憶體,稱為本地執行緒分配快取(Thread Local Allocation Buffer,TLAB)。哪個執行緒要分配記憶體,就在該執行緒對應的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要鎖定同步。虛擬機器是否使用TLAB,可以通過==
物件的記憶體佈局
在HotSpot虛擬機器中,物件在記憶體中儲存地佈局可以分為3塊區域:物件頭(Header),例項資料(Instance Data)和對齊填充(Padding)。其中要注意的是,例項欄位包括自身定義的和從父類繼承下來的(即使父類的例項欄位被子類覆蓋或者被private修飾,都照樣為其分配記憶體)。相信很多人在剛接觸面向物件語言時,總把繼承看成簡單的“複製”,這其實是完全錯誤的。JAVA中的繼承僅僅是類之間的一種邏輯關係(具體如何儲存記錄這種邏輯關係,則設計到Class檔案格式的知識,具體請看我的另一篇博文),唯有建立物件時的例項欄位,可以簡單的看成“複製”。
如果物件是陣列型別,那麼JVM將會用3個字寬度儲存物件頭,如果是非陣列型別,則用2字寬儲存物件頭。在32位的虛擬機器裡面,1字寬是4位元組。
長度 | 內容 | 說明 |
---|---|---|
32/64Bit | Mark Word | 儲存物件的hashCode或鎖資訊 |
32/64Bit | Class Metadata Address | 儲存到物件型別資料的指標 |
32/64Bit | Array length | 陣列的長度(如果物件是陣列) |
Java物件的Mark Word裡預設儲存物件的HashCode、分代年齡和鎖標記位。32位的JVM的Mark Word的預設儲存結構如下所示:
鎖狀態 | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit鎖標誌位 |
---|---|---|---|---|
無鎖狀態 | 物件的hashcode | 物件分代年齡 | 0 | 01 |
在執行期間,Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。Mark Word可能變化為儲存以下4種資料,如下所示:
鎖狀態 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否是偏向鎖 | 鎖標誌位 | ||
輕量級鎖 | 指向棧中鎖記錄的指標 | 00 | |||
重量級鎖 | 指向互斥量(重量級鎖)的指標 | 10 | |||
GC標記 | 空 | 11 | |||
偏向鎖 | 執行緒ID | Epoch | 物件分代年齡 | 1 | 01 |
初始化
記憶體分配完成後,虛擬機器需要將分配到的記憶體空間初始化零值(不包括物件頭),如果使用的是TLAB,這一工作過程也可以提前至TLAB分配時進行。這一過程保證了例項欄位在java程式碼中可以不賦值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。而方法的區域性變數卻必須要顯示初始化後才可以訪問。
接下來,JVM要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的元資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊。這些資訊存放在物件的物件頭中。根據虛擬機器當前的執行狀態的不同,如是否啟用偏向鎖,物件頭會有不同的設定方式。如前面所講。
執行–init方法
在上面工作都完成之後,從虛擬機器的視角來看,一個新的物件已經產生了,但從java程式的視角來看,物件建立才剛剛開始——init方法還沒有執行,所有的欄位都還為零。所以,一般來說,執行new指令之後會接著執行init方法,把物件按照程式設計師的意願進行初始化,如:int a=7;這樣一個真正可用的物件才算完全產生出來。
執行建構函式
執行相應的建構函式