天天看點

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位元組的整數倍,如果對象執行個體資料部分沒有對齊的話,就需要通過對齊填充來補全。