天天看點

一切要從new Object()說起

文章目錄

    • Object類介紹
    • Object的記憶體布局
      • 對象頭(Object Header)
        • mark word
        • class pointer
        • 數組長度
      • 執行個體資料(Instance Data)
      • 對齊填充(Padding)
        • 為什麼要對齊填充?
    • Object占用的記憶體大小
      • 類指針壓縮驗證
        • jol依賴
    • Object的記憶體配置設定
      • 記憶體配置設定流程
      • TLAB
      • 記憶體配置設定的兩種方法
        • 指針碰撞(如Serial、ParNew)
        • 空閑清單(如CMS)
    • Object通路方式
      • 句柄通路
      • 直接指針
    • Object的加載
    • Object的銷毀
    • Object源碼分析
      • Object的主要方法
      • Object的構造方法

Object類介紹

java.lang.Object

類是所有類的父類, Java 的所有類都繼承了 Object,所有類皆可以使用 Object 的方法。

Object類位于

java.lang

包中,編譯時會自動導入,它可以顯示繼承,也可以隐式繼承,如我們建立一個類時,如果沒有明确繼承一個父類,那麼它就會自動繼承 Object,成為 Object的子類。

通常我們需要執行個體化一個Object,需要通過

new

關鍵字去初始化。

Object的記憶體布局

在 JVM 中,一個Object對象通常由以下三部分所組成:

  • 對象頭(Object Header)
  • 執行個體資料(Instance Data)
  • 對齊填充(Padding)

對象頭(Object Header)

包括關于對象指向它的類中繼資料的指針、分代年齡、鎖狀态、同步狀态和辨別哈希碼等基本資訊。由兩個詞mark word和class pointer組成,如果是數組對象的話,還會有一個數組長度。

mark word

一切要從new Object()說起

通常是一組位域,用于存儲對象自身的運作時資料,如hashCode、GC分代年齡、鎖同步資訊等等。占用64個比特,8個位元組。

class pointer

類指針,是對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。占用64個比特,8個位元組。開啟壓縮類指針後,占用32個比特,4個位元組。

數組長度

如果當對象是一個數組對象時,那麼在對象頭中有一個儲存數組長度的空間,占用4位元組(32bit)空間。

執行個體資料(Instance Data)

對象中所有屬性的值,包括從父類繼承下來的屬性。如果對象未定義屬性,則這裡就不會有資料。根據屬性類型的不同占不同的位元組,例如boolean類型占1個位元組,int類型占4個位元組等等。為了提高存儲空間的使用率,這部分資料的存儲順序會受到虛拟機配置設定政策參數和字段在Java源碼中定義順序的影響。

屬性類型分為以下兩種:

  • 基本資料類型:
    類型 占用位元組數
    byte,boolean 1
    char,short 2
    int,float 4
    long,double 8
  • 引用資料類型:

    Object對象及其子類

對齊填充(Padding)

理論上計算機對于任何變量的通路都可以從任意位置開始,然而實際上系統會對這些變量的存放位址有限制,通常将變量首位址設為某個數N的倍數,這就是記憶體對齊填充。

預設情況下,Java虛拟機堆中對象的起始位址需要對齊至8的整數倍。如果一個對象的對象頭和執行個體資料占用的總大小不到8位元組的整數倍,則以此來填充對象大小至8位元組的整數倍,否則就不需要進行記憶體對齊填充。

為什麼要對齊填充?

記憶體對齊的其中一個原因,是讓字段隻出現在同一CPU的緩存行中。如果字段不是對齊的,那麼就有可能出現跨緩存行的字段。也就是說,該字段的讀取可能需要替換兩個緩存行,而該字段的存儲也會同時污染兩個緩存行。這兩種情況對程式的執行效率而言都是不利的。其實對其填充的最終目的是為了計算機高效尋址,提高CPU記憶體通路速度,一般處理器的記憶體存取粒度都是N的整數倍,假如通路M(M不能被N整除)大小的資料,沒有進行記憶體對齊,有可能就需要兩次通路才可以讀取出資料,而進行記憶體對齊可以一次性把資料全部讀取出來。

Object占用的記憶體大小

Java 對象标頭(mark word)大小在 32 位和 64 位 jvm 上有所不同。确切的大小會因 JVM(尤其是處理器架構)而異。也有可能出于對齊目的,JVM 可以實作最小大小,但獲得“前 4 個位元組的字段空閑”政策。例如,在 64 位 JVM 上,類型指針占用 8 個位元組,螢幕資訊占用 4 個位元組。

至于方法,通常來說,對象中方法的數量對其大小沒有影響。方法相關資訊存放于方法區(jdk8叫元空間)通常通過在多個對象之間共享Class的單個執行個體來存儲,然後讓每個對象存儲一個指向對應Class的指針。

下面預設都以Hotspot實作的64位虛拟機為例:

原本情況下,類指針應該占64個比特,也就是8個位元組。但因為Hotspot實作的64位虛拟機,預設會開啟壓縮類指針(和壓縮對象指針不一樣),而類指針就在

class Pointer

中存儲着,是以會把

class Pointer

壓縮成4個位元組。

new Object()

占用大小分為兩種情況:

  • 未開啟壓縮類指針

    8位元組(mark word) + 8位元組(class pointer) = 16位元組

  • 開啟壓縮類指針(預設是開啟的)

    8位元組(mark word) + 4位元組(class pointer) + 4位元組(padding) = 16位元組

假如是一個數組對象,則:

  • 未開啟壓縮類指針

    8位元組(mark word) + 8位元組(class pointer) + 4位元組(padding) + 4位元組(length)= 24位元組

  • 開啟壓縮類指針(預設是開啟的)

    8位元組(mark word) + 4位元組(class pointer) + 4位元組(length)= 16位元組

類指針壓縮驗證

使用

java -XX:+PrintCommandLineFlags -version

指令,如下所示:

一切要從new Object()說起

-XX:+UseCompressedClassPointers

:使用壓縮類指針(注意隻有64位HotSpot VM才支援壓縮)

我們可以使用

java -XX:-UseCompressedClassPointers

關閉指針(換成+号則是開啟),我們可以通過以下代碼進行驗證

import org.openjdk.jol.info.ClassLayout;

public class ObjectDemo {

    static Object o = new Object();
    static Object[] objects = new Object[0];

    public static void main(String[] args) {

        //需要添加jol依賴
        //列印對象的布局資訊
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        System.out.println(ClassLayout.parseInstance(objects).toPrintable());
    }
}
           

輸出如下:

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000355da25401 (hash: 0x355da254; age: 0)
  8   8        (object header: class)    0x000000001be81c00
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

[Ljava.lang.Object; object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000002626b41801 (hash: 0x2626b418; age: 0)
  8   8                    (object header: class)    0x000000001bf3b180
 16   4                    (array length)            0
 16   8                    (alignment/padding gap)   
 24   0   java.lang.Object Object;.<elements>        N/A
Instance size: 24 bytes
Space losses: 8 bytes internal + 0 bytes external = 8 bytes total
           

可以看到,數組對象總大小變成了24Byte

jol依賴

<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.16</version>
            <scope>provided</scope>
        </dependency>
           

Object的記憶體配置設定

記憶體配置設定流程

一切要從new Object()說起

java對象的配置設定政策主要有以下幾步:

  1. JVM會先進行棧上配置設定
  2. 如果沒有開啟棧上配置設定或則不符合條件的則會進行TLAB配置設定
  3. 如果對象滿足了直接進入老年代的條件,那就直接配置設定在老年代
  4. 嘗試在eden區配置設定,如果eden區經過GC後空間仍然不夠配置設定則也會配置設定到老年代(注意開啟空間擔保配置情況下隻要老年代的連續空間大于新生代對象總大小或曆次晉升的平均大小,就會進行Minor GC,否則将進行Full GC)
  5. 配置設定在eden區的對象經過一次GC後存活則進入s區,s區空間不夠則直接進入老年代
  6. s區對象達到一定年齡(預設15)則進入老年代
  7. 對象配置設定到老年代時,若空間不夠則觸發Full GC,若Full GC後記憶體仍然不夠則抛出OOM異常

TLAB

JVM在堆上配置設定記憶體的時候為了保證Java對象的記憶體配置設定的安全性,需要對這個過程做同步操作(CAS機制),為了提高效率,每一個線程在Java堆中能夠預先配置設定一小塊記憶體,這部份記憶體稱之為TLAB(Thread Local Allocation Buffer),這塊記憶體的配置設定時線程獨占的,讀取、使用、回收是線程共享的。而且隻有經過了記憶體逃逸分析的對象才會配置設定到TLAB中去。

能夠經過設定-XX:+/-UseTLAB參數來指定是否開啟TLAB配置設定。

記憶體配置設定的兩種方法

為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來。

指針碰撞(如Serial、ParNew)

假設Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閑的記憶體放在另一邊,中間放着一個指針作為分界點的訓示器,那所配置設定記憶體就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種配置設定方式稱為“指針碰撞”(Bump the Pointer)。

空閑清單(如CMS)

如果Java堆中的記憶體并不是規整的,已使用的記憶體和空閑的記憶體互相交錯,那就沒有辦法簡單地進行指針碰撞了,虛拟機就必須維護一個清單,記錄上哪些記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄,這種配置設定方式稱為“空閑清單”(Free List)。

Object通路方式

句柄通路

  • 原理:在堆中建立句柄池,棧中對象引用 -> 句柄池句柄 ( 句柄資訊1 -> 堆中對象執行個體; 句柄資訊2 -> 方法區中對象對應的類資料)
  • 優勢:GC時大量移動對象,隻需要修改句柄池中引用,無需修改棧中對象引用,減少引用的維護。
  • 缺點:對象通路時多一步指針定位的時間開銷,Java程式對象通路很多,效率有一定降低。

直接指針

這也是Hotspot預設的通路方式

  • 原理:棧中對象引用 -> 堆中對象執行個體 -> 方法區對象對應的類資料
  • 優勢:減少對象通路的一次指針定位的時間開銷,Java程式對象通路很多,效率有一定提升。
  • 缺點:GC時大量移動對象,需要修改所有關聯的棧中對象引用

Object的加載

請參考:JVM雙親委派模型

Object的銷毀

請參考:JVM垃圾回收機制深入解析

Object源碼分析

Object的主要方法

Object 類在 java.lang 包中,是所有類的父類,所有類都間接或者直接繼承 Object 類。

Object 類中方法主要有:

  • registerNatives():本地方法
  • getClass():傳回對象對應的Class類
  • **hashCode():**傳回對象的哈希碼
  • equals():預設判斷對象的位址是否相同
  • **clone():**實作拷貝必須實作 Cloneable 接口,并重寫 clone()方法。
  • toString():顯示的是類名 +"@"+ 對象哈希碼的十六進制數
  • notify():喚醒同一個對象上調用 wait()方法的線程的其中一個
  • notifyAll():會喚醒同一個對象上所有調用 wait()方法的線程
  • wait():釋放鎖,并使目前線程進入阻塞
  • finalize():當垃圾回收器将要回收對象所占記憶體之前被調用

其中registerNatives()、getClass()、clone()、notify()、notifyAll()和 wait()等方法是本地方法,具體實作是通過c/c++代碼實作的,是以子類不能重寫。

Object的構造方法

源碼中并沒有Object的構造方法,但是,編譯器在編譯期間會給Object一個預設的空的構造方法(任意Java類,隻要類中沒有構造方法,編譯器都會預設的給一個空構造方法,若已有構造方法,則不會添加):

public Object(){
}
           

乍一看啥内容也沒有,我們用反編譯手段探探究竟:

0 return
           

Object的構造方法實際上就執行了一條

return

的彙編指令,但是如果一個對象中存在其它屬性的話,則JVM還會為其調用一個init方法,比如下面這個類:

public class MyObject(){
	Object o = new Object();
}
           

反彙編後發現它的構造器中的指令為:

0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 return
           

也就是說對象中的執行個體屬性會通過Object中的init方法進行初始化,該方法由JVM建立,如果不存在執行個體變量,也就不會建立init方法了。