你有認真瞭解過本身的「Java對象」嗎? 渣男

對象在 JVM 中是怎麼存儲的html

對象頭裏有什麼?java

文章收錄在 GitHub JavaKeeper ,N線互聯網開發必備技能兵器譜,有你想要的。git

做爲一名 Javaer,生活中的咱們可能暫時沒有對象,可是工做中天天都會建立大量的 Java 對象,你有試着去了解下本身的「對象」嗎?程序員

咱們從四個方面從新認識下本身的「對象」github

  1. 建立對象的 6 種方式
  2. 建立一個對象在 JVM 中都發生了什麼
  3. 對象在 JVM 中的內存佈局
  4. 對象的訪問定位

1、建立對象的方式

  • 使用 new 關鍵字算法

    這是建立一個對象最通用、常規的方法,同時也是最簡單的方式。經過使用此方法,咱們能夠調用任何要調用的構造函數(默認使用無參構造函數)數組

    Person p = new Person();
    複製代碼
  • 使用 Class 類的 newInstance(),只能調用空參的構造器,權限必須爲 public安全

    //獲取類對象
    Class aClass = Class.forName("priv.starfish.Person");
    Person p1 = (Person) aClass.newInstance();
    複製代碼
  • Constructor 的 newInstance(xxx),對構造器沒有要求網絡

    Class aClass = Class.forName("priv.starfish.Person");
    //獲取構造器
    Constructor constructor = aClass.getConstructor();
    Person p2 = (Person) constructor.newInstance();
    複製代碼
  • clone()併發

    深拷貝,須要實現 Cloneable 接口並實現 clone(),不調用任何的構造器

    Person p3 = (Person) p.clone();
    複製代碼
  • 反序列化

    經過序列化和反序列化技術從文件或者網絡中獲取對象的二進制流。

    每當咱們序列化和反序列化對象時,JVM 會爲咱們建立了一個獨立的對象。在 deserialization 中,JVM 不使用任何構造函數來建立對象。(序列化的對象須要實現 Serializable)

    //準備一個文件用於存儲該對象的信息
    File f = new File("person.obj");
    FileOutputStream fos = new FileOutputStream(f);
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    //序列化對象,寫入到磁盤中
    oos.writeObject(p);
    //反序列化
    FileInputStream fis = new FileInputStream(f);
    ObjectInputStream ois = new ObjectInputStream(fis);
    //反序列化對象
    Person p4 = (Person) ois.readObject();
    複製代碼
  • 第三方庫 Objenesls

    Java已經支持經過 Class.newInstance() 動態實例化 Java 類,可是這須要Java類有個適當的構造器。不少時候一個Java類沒法經過這種途徑建立,例如:構造器須要參數、構造器有反作用、構造器會拋出異常。Objenesis 能夠繞過上述限制

2、建立對象的步驟

這裏討論的僅僅是普通 Java 對象,不包含數組和 Class 對象(普通對象和數組對象的建立指令是不一樣的。建立類實例的指令:new,建立數組的指令:newarray,anewarray,multianewarray)

1. new指令

虛擬機遇到一條 new 指令時,首先去檢查這個指令的參數是否能在 Metaspace 的常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過(即判斷類元信息是否存在)。若是沒有,那麼須在雙親委派模式下,先執行相應的類加載過程。

2. 分配內存

接下來虛擬機將爲新生代對象分配內存。對象所需的內存的大小在類加載完成後即可徹底肯定。若是實例成員變量是引用變量,僅分配引用變量空間便可,即 4 個字節大小。分配方式有「指針碰撞(Bump the Pointer)」和「空閒列表(Free List)」兩種方式,具體由所採用的垃圾收集器是否帶有壓縮整理功能決定。

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

  • 若是內存是不規整的,虛擬機須要維護一個列表,這個列表會記錄哪些內存是可用的,在爲對象分配內存的時候從列表中找到一塊足夠大的空間劃分給該對象實例,並更新列表內容,這種分配方式就是「空閒列表」。使用CMS 這種基於Mark-Sweep 算法的收集器時,一般採用空閒列表。

咱們都知道堆內存是線程共享的,那在分配內存的時候就會存在併發安全問題,JVM 是如何解決的呢?

通常有兩種解決方案:

  1. 對分配內存空間的動做作同步處理,採用 CAS 機制,配合失敗重試的方式保證更新操做的原子性

  2. 每一個線程在 Java 堆中預先分配一小塊內存,而後再給對象分配內存的時候,直接在本身這塊"私有"內存中分配,當這部分區域用完以後,再分配新的"私有"內存。這種方案稱爲 TLAB(Thread Local Allocation Buffer),這部分 Buffer 是從堆中劃分出來的,可是是本地線程獨享的。

    **這裏值得注意的是,咱們說 TLAB 是線程獨享的,只是在「分配」這個動做上是線程獨佔的,至於在讀取、垃圾回收等動做上都是線程共享的。並且在使用上也沒有什麼區別。**另外,TLAB 僅做用於新生代的 Eden Space,對象被建立的時候首先放到這個區域,可是新生代分配不了內存的大對象會直接進入老年代。所以在編寫 Java 程序時,一般多個小的對象比大的對象分配起來更加高效。

    虛擬機是否使用 TLAB 是能夠選擇的,能夠經過設置 -XX:+/-UseTLAB 參數來指定,JDK8 默認開啓。

3. 初始化

內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。如:byte、short、long 轉化爲對象後初始值爲 0,Boolean 初始值爲 false。

4. 對象的初始設置(設置對象的對象頭)

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

5. <init>方法初始化

在上面的工做都完成了以後,從虛擬機的角度看,一個新的對象已經產生了,可是從 Java 程序的角度看,對象建立纔剛剛開始,<init>方法尚未執行,全部的字段都還爲零。初始化成員變量,執行實例化代碼塊,調用類的構造方法,並把堆內對象的地址賦值給引用變量。

因此,通常來講,執行 new 指令後接着執行 init 方法,把對象按照程序員的意願進行初始化(應該是將構造函數中的參數賦值給對象的字段),這樣一個真正可用的對象纔算徹底產生出來。

3、對象的內存佈局

在 HotSpot 虛擬機中,對象在內存中存儲的佈局能夠分爲 3 塊區域:對象頭(Header)、實例數據(Instance Data)、對其填充(Padding)。

對象頭

HotSpot 虛擬機的對象頭包含兩部分信息。

  • 第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。
  • 對象的另外一部分類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例(並非全部的虛擬機實現都必須在對象數據上保留類型指針,也就是說,查找對象的元數據信息並不必定要通過對象自己)。

若是對象是一個 Java 數組,那在對象頭中還必須有一塊用於記錄數組長度的數據。

元數據:描述數據的數據。對數據及信息資源的描述信息。在 Java 中,元數據大多表示爲註解。

實例數據

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中定義的各類類型的字段內容,不管從父類繼承下來的,仍是在子類中定義的,都須要記錄起來。這部分的存儲順序會受虛擬機默認的分配策略參數和字段在 Java 源碼中定義的順序影響(相同寬度的字段老是被分配到一塊兒)。

規則:

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

對齊填充

對齊填充部分並非必然存在的,也沒有特別的含義,它僅僅起着佔位符的做用。因爲 HotSpot VM 的自動內存管理系統要求對象的起始地址必須是 8 字節的整數倍,也就是說,對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1倍或者2倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

咱們經過一個簡單的例子加深下理解

public class PersonObject {
    public static void main(String[] args) {
        Person person = new Person();
    }
}
複製代碼
public class Person {
    int id = 1008;
    String name;
    Department department;
    {
        name = "匿名用戶";   //name賦值爲字符串常量
    }
}
複製代碼
public class Department {
    int id;
    String name;
}
複製代碼

4、對象的訪問定位

咱們建立對象的目的,確定是爲了使用它,那 JVM 是如何經過棧幀中的對象引用訪問到其內存的對象實例呢?

因爲 reference 類型在 Java 虛擬機規範裏只規定了一個指向對象的引用,並無定義這個引用應該經過哪一種方式去定位,以及訪問到 Java 堆中的對象的具體位置,所以不一樣虛擬機實現的對象訪問方式會有所不一樣,主流的訪問方式有兩種:

  • 句柄訪問

    若是使用句柄訪問方式,Java堆中會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息。使用句柄方式最大的好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference自己不須要被修改。

  • 直接指針(Hotspot 使用該方式)

    若是使用該方式,Java堆對象的佈局就必須考慮如何放置訪問類型數據的相關信息,reference中直接存儲的就是對象地址。使用直接指針方式最大的好處就是速度更快,他節省了一次指針定位的時間開銷

參考:

相關文章
相關標籤/搜索