Java對象的創建過程


Java對象的創建過程

  1. 當Java虛擬機遇到一條字節碼new指令時,首先會去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程

  2. 在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需內存的大小在類加載完成后便可完全確定,為對象分配空間的任務實際上便等同於把一塊確定大小的內存塊從Java堆中划分出來。

  3. 內存分配完成后,虛擬機必須將分配到的內存空間(不包含對象頭)都初始化為零值(如果使用了TLAB的話,這一項工作也可以提前至TLAB分配時順便進行TLAB為本地線程分配緩沖 詳解可見下文)。這步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問到這些字段的數據類型所對應的零值。

  4. 接下來,Java虛擬機還要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息保存在對象的對象頭中。根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。

  5. 至此,從虛擬機的角度來看,一個新的對象已經產生。然而從Java程序的角度來看,對象創建才剛剛開始--->構造函數,即Class文件中的<init>()方法還沒有執行,所有的字段都為默認的零值,對象需要的其他資源和狀態信息也還沒有按照預定的意圖構造好。一般來說,new指令之后會接着執行<init>()方法,按照程序員的意願對對象進行初始化,這樣一個真正可用的對象才算完全被構造出來。

類加載的執行過程

  1. 加載--主要是將.class文件中的二進制字節流讀入到新JVM中

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

    1. 驗證--確保加載進來的字節流符合JVM規范
      • 文件格式驗證
      • 元數據驗證,是否符合java語言規范
      • 字節碼驗證,確保程序語義合法,符合邏輯
      • 符號引用驗證,確保下一步的解析能正常執行
    2. 准備--為靜態變量在方法區分配內存,並設置默認初始值
    3. 解析--虛擬機將常量池內的符號引用替換為直接引用
      符號引用:符號引用與虛擬機實現的布局無關,引用的目標並不一定要已經加載到內存中。各種虛擬機實現的內存布局各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規范的Class文件格式中。
      直接引用:直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。
  3. 初始化--標記為常量值的字段賦值的過程,只對static修飾的變量或語句塊進行初始化。
    初始化階段是執行類構造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變量的賦值操作和靜態語句塊中的語句合並而成的。虛擬機會保證子<client>方法執行之前,父類的<client>方法已經執行完畢,如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那么編譯器可以不為這個類生成<client>()方法。

注意以下幾種情況不會執行類初始化:

  1. 通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
  2. 定義對象數組,不會觸發該類的初始化。
  3. 常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。
  4. 通過類名獲取 Class 對象,不會觸發類的初始化。
  5. 通過 Class.forName 加載指定類時,如果指定參數 initialize 為 false 時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
  6. 通過 ClassLoader 默認的 loadClass 方法,也不會觸發初始化動作。

內存的分配方式

內存的分配方式有以下兩種:

  1. 指針碰撞
    假設堆中內存是絕對規整的,所有被使用過的內存都被放在一邊,空閑的內存放在另一邊,中間放着一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的距離。
  2. 空閑列表
    如果堆中內存並不是規整的,已被使用的內存和空閑的內存相互交錯在一起,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄。

選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由采用的垃圾收集器是否帶有空間壓縮整理的能力決定。

因此,當使用Serial、ParNew等帶壓縮整理過程的收集器時,系統采用的分配算法是指針碰撞,即簡單又高效。
而當使用CMS這種基於清除(Sweep)算法的收集器時,理論上就只能采用較為復雜高效的空閑列表來分配內存。

指針碰撞方式存在的問題:
對象創建在虛擬機中是非常頻繁的行為,僅僅修改一個指針所指向的位置,在並發情況下也並不是線程安全的。
可能會出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。解決這個問題有兩種可選方案:

  1. 對分配內存空間的動作進行同步處理---實際上虛擬機是采用CAS配上失敗重試的方式保證更新操作的原子性。
  2. 把內存分配的動作按照線程划分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(TLAB),哪個線程要分配內存,就在哪個線程的本地緩沖區中分配,只有本地緩沖區用完了,分配新的緩沖區時才需要同步鎖定。

對象的內存布局

由於Java面向對象的思想,在JVM中需要大量存儲對象,存儲時為了實現一些額外的功能,需要在對象中添加一些標記字段用於增強對象功能,這些標記字段組成了對象頭。
Hotspot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)
MarkWord:默認存儲對象的HashCode,分代年齡和鎖標志位信息。這些信息都是與對象自身定義無關的數據,所以Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲盡量多的數據。它會根據對象的狀態復用自己的存儲空間,也就是說在運行期間Mark Word里存儲的數據會隨着鎖的標志位的變化而變化。
Klass Point:對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

實例數據部分是對象真正存儲的有效信息,即我們在程序代碼里面所定義的各種類型的字段內容,無論是從父類繼承下來的還是在子類中定義的字段都必須記錄起來。

對其填充不是必然存在的,也沒有特別的含義,它僅僅起占位符的作用。由於任何對象的大小都必須是8字節的整數倍,如果對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM