對象實例化內存佈局與訪問定位

對象實例化

對象建立方式

  1. new:最多見的方式、單例類中調用java

    • 變形1: getInstance的靜態類方法
    • 變形2 : XXXBuilder/XXXFactory 的靜態方法
  2. Class 的 newInstance 方法:在 JDK9 裏面被標記爲過期的方法,由於只能調用空參構造器程序員

  3. Constructor 的 newInstance(XXX):反射的方式,能夠調用空參的,或者帶參的構造器算法

  4. 使用 clone():不調用任何的構造器,要求當前的類須要實現 Cloneable 接口中的 clone 接口數組

  5. 使用序列化:序列化通常用於 Socket 的網絡傳輸網絡

  6. 第三方庫 Objenesis併發

建立對象的步驟

1.判斷對象對應的類是否加載、連接、初始化

虛擬機遇到一條new指令,首先去檢查這個指令的參數可否在Metaspace的常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載,解析和初始化。(即判斷類元信息是否存在)。若是沒有,那麼在雙親委派模式下,使用當前類加載器以ClassLoader + 包名 + 類名爲key進行查找對應的 .class文件,若是沒有找到文件,則拋出ClassNotFoundException異常,若是找到,則進行類加載,並生成對應的Class對象。佈局

2.爲對象分配內存

首先計算對象佔用空間大小,接着在堆中劃分一塊內存給新對象。 若是實例成員變量是引用變量,僅分配引用變量空間便可,即4個字節大小。測試

若是內存規整,使用指針碰撞

那麼虛擬機將採用的是指針碰撞法(Bump The Point)來爲對象分配內存。ui

意思是全部用過的內存在一邊,空閒的內存放另一邊,中間放着一個指針做爲分界點的指示器,分配內存就僅僅是把指針指向空閒那邊挪動一段與對象大小相等的距離罷了。若是垃圾收集器選擇的是Serial 、ParNew這種基於壓縮算法的,虛擬機採用這種分配方式。通常使用帶Compact(整理)過程的收集器時,使用指針碰撞。spa

若是內存不規整

虛擬機須要維護一個列表,空閒列表分配

若是內存不是規整的,已使用的內存和未使用的內存相互交錯,那麼虛擬機將採用的是空閒列表來爲對象分配內存。意思是虛擬機維護了一個列表,記錄上那些內存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的內容。這種分配方式成爲了 「空閒列表(Free List)」

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

3.處理併發問題

採用CAS配上失敗重試保證更新的原子性

每一個線程預先分配TLAB - 經過設置 -XX:+UseTLAB參數來設置(區域加鎖機制)

  • 在Eden區給每一個線程分配一塊區域

4.初始化分配到的內存(零值初始化)

給對象屬性賦值的操做,全部屬性設置默認值,保證對象實例字段在不賦值能夠直接使用。

5.設置對象的對象頭

將對象的所屬類(即類的元數據信息)、對象的HashCode和對象的GC信息、鎖信息等數據存儲在對象的對象頭中。這個過程的具體設置方式取決於JVM實現。

6.執行init方法進行初始化

在Java程序的視角看來,初始化才正式開始。初始化成員變量,執行實例化代碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變量

所以通常來講(由字節碼中跟隨invokespecial指令所決定),new指令以後會接着就是執行方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完成建立出來。

代碼示例

/**
 * 測試對象實例化的過程
 * 
 * ① 加載類元信息 - ② 爲對象分配內存 - ③ 處理併發問題 - ④ 屬性的默認初始化(零值初始化) - ⑤ 設置對象頭的信息 - ⑥
 * 屬性的顯式初始化、代碼塊中初始化、構造器中初始化
 * 
 * 給對象的屬性賦值的操做: ① 屬性的默認初始化 - ② 顯式初始化 / ③ 代碼塊中初始化 - ④ 構造器中初始化
 * 
 * @author kiser
 */
public class Customer {
    int id = 1001;
    String name;
    Account acct;

    {
        name = "匿名客戶";
    }

    public Customer() {
        acct = new Account();
    }
}

class Account {

}

對象內存佈局

對象頭(Header)

包含兩部分

  • 運行時元數據
    • 哈希值( HashCode )
    • GC分代年齡
    • 鎖狀態標誌
    • 線程持有的鎖
    • 偏向線程ID
    • 偏向時間戳
  • 類型指針:指向類元數據的InstanceKlass,肯定該對象所屬的類型
  • 說明:若是是數組,還需記錄數組的長度

實例數據(Instance Data)

說明:它是對象真正存儲的有效信息,包括程序代碼中定義的各類類型的字段(包括從父類繼承下來的和自己擁有的字段) 規則:

  • 相同寬度的字段總被分配在一塊兒
  • 父類中定義的變量會出如今子類以前
  • 若是CompactFields參數爲true(默認爲true),子類的窄變量可能插入到父類變量的空隙

對齊填充(Padding)

不是必須的,也沒特別含義,僅僅起到佔位符做用

舉例

class Account {

}

public class Customer {
    Account acct;
    int id = 1001;
    String name;

    {
        name = "匿名客戶";
    }

    public Customer() {
        acct = new Account();
    }
}
/**
 * @author kiser
 *
 */
public class CustomerTest {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Customer cust = new Customer();
    }

}

對象的訪問定位

JVM是如何經過棧幀中的對象引用訪問到其內部的對象實例呢?

對象訪問的兩種方式

句柄訪問

句柄訪問就是說棧的局部變量表中,記錄的對象的引用,而後在堆空間中開闢了一塊空間,也就是句柄池。

優勢

reference中存儲穩定句柄地址,對象被移動(垃圾收集時移動對象很廣泛)時只會改變句柄中實例數據指針便可,reference自己不須要被修改

直接指針(HotSpot採用)

直接指針是局部變量表中的引用,直接指向堆中的實例,在對象實例中有類型指針,指向的是方法區中的對象類型數據。

相關文章
相關標籤/搜索