JVM-物件及物件記憶體佈局

傑哥很忙發表於2021-03-05


目錄

JVM-執行時資料區域
JVM-物件及其記憶體佈局

前言

上一篇文章對JVM的執行時資料區域的內容進行了梳理,本篇文章對JVM中的物件和物件的記憶體佈局進行深入解析。本文參考了《深入理解Java虛擬機器》、《深入解析Java虛擬機器HotSpot》、《HotSpot實戰》三本書。

下面提到的虛擬機器都特指JDK1.8版本的HotSpot VM,其他虛擬機器的實現有可能不太一樣。

類與物件

在編譯時,通過Javac編譯器為虛擬機器規範的class檔案格式。class檔案格式是與作業系統和機器指令集無關的、平臺中立的格式。其他語言編寫的程式碼只需要實現指定語言的編譯器編譯位JVM規範標準的class檔案就可以實現該語言執行在JVM之上,這就是JVM的語言無關性。

20210207102626.png

通過java命令執行class檔案,首先會通過類載入器將class檔案載入到記憶體中,載入class檔案會為類生成一個klass例項。在klass包含了用於描述Java類的後設資料,包括欄位個數、大小、是否為陣列、是否有父類、方法資訊等。

物件類二分模型

HotSpot虛擬機器是使用C++實現的, C++也是面嚮物件語言。可以採用java類一一對映到C++類,當建立Java物件就建立對應的C++類的物件。

20210303112341.png

但是由於如果C++的物件含有虛擬函式,則建立的物件會有虛方法表指標,指向虛方法表。如果採用這種直接一對一對映的方式,會導致含有虛方法的類建立的物件都包含虛方法指標。因此在HotSpot虛擬機器中,通過物件類二分模型,將類描述資訊和例項資料進行拆分。使用僅包含資料不包含方法的oop(Ordinary Object Pointer)物件描述Java的物件,使用klass描述java的類。oop的職責在於表示物件例項資料,沒必要維護虛擬函式指標。通過oop物件頭部的一個指標指向對應的klass物件進行關聯。

20210303112101.png

在HotSpot虛擬機器中,普通物件的類通過instanceKlass表示,物件例項則通過instanceOopDesc表示。

20210130125737.png

在JVM中引用型別可以分為物件,基本型別陣列和物件型別陣列。可以分別對映到Java中的對應的物件和型別。

物件
物件 instanceKlass instanceOopDesc
基本型別陣列 typeArrayKlass typeArrayOopDesc
物件型別陣列 objArrayKlass objArrayOopDesc

除了常用的3類引用物件外,還有一些其他JVM自己要用的java.lang.ClassLoaderInstanceClassLoaderKlass描述,java.lang.ClassInstanceMirrorKlass描述等。

物件

HotSpot VM使用oop描述物件,oop字面意思是“普通物件指標”。它是指向一片記憶體的指標,只是將這片記憶體‘視作’(強制型別轉換)Java物件/陣列。物件的本質就是用物件頭和欄位資料填充這片記憶體。

物件記憶體佈局

20210131165506.png

JOL工具

在談論具體物件佈局時,推薦一個JOL工具,可以列印物件的記憶體佈局。通過maven引入。

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

通過ClassLayout.parseInstance(new Object()).toPrintable()即可列印物件的記憶體佈局。

物件頭

普通物件的物件頭包含2部分,第一部分被稱為Mark Word,第二部分為型別指標。如果物件為陣列,除了普通物件的兩部分外物件頭還包含陣列長度。下圖是64位虛擬機器物件頭。

20210303134524.png

32位虛擬機器頭部的Mark Word長度為4個位元組。

Mark Word

Mark Word儲存了物件執行時必要的資訊,包括雜湊碼(HashCode)、GC分代年齡、偏向狀態、鎖狀態標誌、偏向執行緒ID、偏向時間戳等資訊。通過型別指標,可以找到物件對應的型別資訊。32位虛擬機器和64位虛擬機器的Mark Word長度分別為4位元組和8位元組。

不論是32位還是64位虛擬機器的物件頭部都使用了4位元記錄分代年齡,每次GC時物件倖存年齡都會加1,因此物件在survivor區最多幸存15次,超過15次時,仍然有可達根的物件就會從survivor區被轉移到老年代。可以通過-XX:MaxTenuringThreshold=15引數修改最大幸存年齡。

CMS垃圾回收器預設為6次。

型別控制程式碼

相比32位物件頭大小,64位物件頭更大一些,64位虛擬機器物件頭的Mark Word型別指標地址都是8位元組。而通常情況,我們的程式不需要佔用那麼大的記憶體。因此虛擬機器通過壓縮指標功能,將物件頭的型別指標進行壓縮。而Mark Word由於執行時需要儲存的頭部資訊會大於4位元組,仍然使用8位元組。若配置開啟了-XX:+UseCompressedOops,虛擬機器會將型別指標地址壓縮為32位。若配置開啟了-XX:+UseCompressedClassPointers,則會壓縮klass物件的地址為32位。

需要注意的是,當地址經過壓縮後,定址範圍不可避免的會降低。對於64位CPU,由於目前記憶體一般到不了2^64,因此大多數64位CPU的地址匯流排實際會小於64位,比如48位。
開啟-XX:+UseCompressedOops,預設也會開啟-XX:+UseCompressedClassPointers。關閉-XX:+UseCompressedOops,預設也會關閉-XX:+UseCompressedClassPointers
如果開啟-XX:+UseCompressedOops,但是關閉-XX:+UseCompressedClassPointers,啟動虛擬機器的時候會提示“Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops”。

20210303135259.png

普通物件記憶體佈局(64位虛擬機器指標壓縮時)


System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)            Mark Word
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)            
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)   型別指標
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

需要注意,由於記憶體按小端模式分佈,因此顯示的內容是反著的。上面實際物件頭內容為 00000000 00000001 f80001e5

陣列物件記憶體佈局(64位虛擬機器指標壓縮時)

[Ljava.lang.Object; object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)            Mark Word
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           f5 22 00 f8 (11110101 00100010 00000000 11111000) (-134208779)   型別指標
     12     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)            陣列長度
     16     4   java.lang.Object Object;.<elements>                        N/A                                                              陣列元素
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

物件頭與鎖膨脹

物件頭中儲存了鎖的必要資訊,不同的鎖的物件頭儲存內容稍有不同。32位物件頭儲存格式如下

20210131132647.png

JVM底層對加鎖進行了效能優化,預設虛擬機器啟動後大約4秒會開啟偏向鎖功能。當虛擬機器未啟用偏向鎖時,鎖的演化過程為無鎖->輕量鎖(自旋鎖)->重量鎖
當虛擬機器啟用了偏向鎖時,鎖的演化過程為無鎖->偏向鎖->輕量鎖(自旋鎖)->重量鎖

本文不討論JVM對加鎖的具體優化邏輯,內容比較多,感興趣的可以看同學可以參考《淺談偏向鎖、輕量級鎖、重量級鎖》。

20210303113752.png

無鎖

當物件未加鎖時,鎖狀態為01,32位虛擬機器的物件頭部如圖所示

20210303162241.png


public static void main(String[] args) {
    System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
    /*java.lang.Object object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
    0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
    4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
    12    4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total*/
}

需要注意的是其中物件頭儲存的hashCode被稱為identityHashCode,當我們呼叫物件的hashCode方法,返回的就是該值。若我們重寫了hashCode的值,物件頭的hashCode值仍然是內部的identityHashCode,而不是我們重寫的hashCode值。可以通過System.identityHashCode列印identityHashCode,或者也可以通過toString直接列印物件輸出16進位制的identityHashCode


public static void main(String[] args){
    Object obj1 = new Object();
    System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
    System.out.println(obj1.hashCode());
    System.out.println(System.identityHashCode(obj1));
    System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
    System.out.println(obj1);

    /*
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

1766822961
1766822961
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 31 94 4f (00000001 00110001 10010100 01001111) (1335111937)
      4     4        (object header)                           69 00 00 00 (01101001 00000000 00000000 00000000) (105)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object@694f9431 十進位制即為1766822961,二進位制為01101001 01001111 10010100 00110001
    */
}
偏向鎖

偏向鎖中的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是這個鎖會偏向於第一個獲得它的執行緒,如果在接下來的執行過程中,該鎖一直沒有被其他的執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。

偏向鎖的鎖狀態和未鎖狀態一樣都是01,當物件處於偏向狀態時,偏向標記為1;當物件處於未偏向時,偏向標記為0

32位虛擬機器的偏向鎖物件頭部如圖所示

20210303161955.png

偏向時間戳,它實際表示偏向的有效期。

無鎖狀態升級為偏向鎖的條件:

  • 物件可偏向,物件未加鎖時,執行CAS更新物件頭部執行緒偏向執行緒ID為當前執行緒成功。
  • 物件可偏向,物件已加鎖,但偏向執行緒ID為空,執行CAS更新物件頭部執行緒偏向執行緒ID為當前執行緒成功。
  • 物件可偏向,物件已加鎖,且偏向執行緒ID等於當前執行緒ID。
  • 物件可偏向,物件已加鎖,且偏向執行緒ID不為空且不等於當前執行緒ID,執行CAS更新物件頭部執行緒偏向執行緒ID為當前執行緒成功。

虛擬機器啟動時,會根據-XX:BiasedLockingStartupDelay配置延遲啟動偏向,在JDK1.8中,預設為4秒。有需要時可以通過-XX:BiasedLockingStartupDelay=0關閉延時偏向。

//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    /*
    OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
        0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
        4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
        8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
        12     4        (loss due to the next object alignment)
        */
    synchronized (o) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        /*
    OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
        0     4        (object header)                           05 28 9c 02 (00000101 00101000 10011100 00000010) (43788293)
        4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
        8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
        12     4        (loss due to the next object alignment)
    */
    }
}
輕量級鎖

輕量級鎖的鎖狀態為00,32位虛擬機器的輕量級鎖頭部格式如下

20210303163552.png

升級為輕量級鎖條件:

  • 物件不可偏向,跳過偏向鎖直接使用輕量級鎖。
  • 物件可偏向,但偏向加鎖失敗(存線上程競爭)。
  • 物件獲取呼叫hashCode後加鎖。
  • 物件已升級為重量級鎖後,鎖降級只能降級為輕量級鎖,無法降級為偏向鎖。

輕量級鎖會線上程的棧幀中開闢一個鎖記錄區域,將當前物件的頭部儲存在鎖記錄區域中,將鎖記錄區域的地址儲存到當前物件頭部。

20210303163623.png

  • 物件不可偏向直接升級到輕量鎖
public static void main(String[] args){
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
/*
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
*/
synchronized (o) {
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    /*  
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
    0     4        (object header)                           58 f4 dd 02 (01011000 11110100 11011101 00000010) (48100440)
    4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
    12     4        (loss due to the next object alignment)
*/
}
  • 偏向鎖競爭升級為輕量鎖
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
    final Object o = new Object();
    Thread thread= new Thread(){
        @Override
        public void run() {
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable()); //偏向鎖
                /*
    OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
        0     4        (object header)                           05 e8 31 28 (00000101 11101000 00110001 00101000) (674359301)
        4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
        8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
        12     4        (loss due to the next object alignment)
                */
            }
        }
    };
    thread.start();
    thread.join();
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable()); //輕量鎖
        /*
        OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
        0     4        (object header)                           60 f4 b4 02 (01100000 11110100 10110100 00000010) (45413472)
        4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
        8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
        */
    }
}
  • 偏向後呼叫hashCode方法升級為輕量級鎖
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
    final Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable()); //偏向鎖
    /*
    OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
    0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
    4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
    12     4        (loss due to the next object alignment)
    */
    o.hashCode();
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        /*
    OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
    0     4        (object header)                           18 f3 4a 02 (00011000 11110011 01001010 00000010) (38466328)
    4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
        */
    }
}
重量級鎖

輕量級鎖的鎖狀態為10,32位重量級鎖頭部如圖所示

20210303172244.png

輕量級鎖自迴圈一定次數後一致獲取不到鎖,則升級為重量級鎖條件。自旋次數預設為10次,可以通過-XX:PreBlockSpin配置修改次數。

//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {
    final Object o = new Object();
    Thread thread= new Thread(){
        @Override
        public void run() {
            synchronized (o){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(ClassLayout.parseInstance(o).toPrintable()); 
                /*
                 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
                    0     4        (object header)                           2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
                    4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
                    8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
                */
            }
        }
    };
    thread.start();
    synchronized (o){
        Thread.sleep(1000);
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        /*
    OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
        */
    }
    thread.join();
}
重量級鎖降級

當重量級鎖解鎖後就會進行鎖降級,鎖降級只能降級為輕量鎖,無法再使用偏向鎖。

//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {
    final Object o = new Object();
    Thread thread= new Thread(){
        @Override
        public void run() {
            synchronized (o){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(ClassLayout.parseInstance(o).toPrintable()); 
                /*
                 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
                    0     4        (object header)                           2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
                    4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
                    8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
                */
            }
        }
    };
    thread.start();
    synchronized (o){
        Thread.sleep(1000);
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        /*
    OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
        */
    }
    thread.join();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    /*
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
    */
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        /*
         OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
            0     4        (object header)                           98 f6 8c 02 (10011000 11110110 10001100 00000010) (42792600)
            4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
            8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
        */
    }
}

例項資料

物件例項資料預設按照long、double、int、short、char、byte、boolean、reference順序佈局,相同欄位寬度總是分配在一起。若有父物件,則父物件的例項欄位在子物件前面。
另外如果HotSpot虛擬機器的 +XX:CompactFields引數值為true(預設就為true),那子類之中較窄的變數也允許插入父類變數的空隙之中,以節省出一點點空間。

填充

JVM中,物件大小預設為8的整數倍,若物件大小不能被8整除,則會填充空位元組來填充物件保證。

物件生命週期

在瞭解完物件頭部後,我們看下物件的建立的時候發生了什麼事情。當我們呼叫new Object()建立一個物件時,生成的位元組碼如下

0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>

首先通過new指令分配物件,並將物件地址入棧,通過dup指令複製一份棧頂元素。通過invokespecial指令呼叫物件的init進行初始化會消耗棧頂2個槽。由於init方法需要傳入一個引數,該引數即為引用物件本身。在init初始化時會將this指標進行賦值。這樣我們在程式碼中就可以通過this指向當前物件。

物件建立流程如下圖所示。

20210304135752.png

  • 棧上分配
    通常物件都是在堆上建立的,若物件僅在當前作用域下使用,那麼使用完很快就會被GC回收。JVM通過逃逸分析對物件作用域進行分析,如果物件僅在當前作用域下使用,則將物件的例項資料分配在棧上,從而提升物件建立速度的同時減少GC回收的物件數量。

  • 執行緒區域性緩衝區(TLAB)
    如果無法在棧上分配,則物件會在堆上分配。對於JDK1.8來說,Java堆通常使用分代模型,(關於GC,垃圾回收演算法等這裡不做具體討論)。經過統計,90%的物件在使用完成後都會被回收,因此預設新生代會分配10%的空間給倖存者區。
    20210304152949.png
    物件先在eden區進行分配,但是我們知道,堆是所有執行緒共享的區域,會存在多執行緒併發問題。因此在堆上分配就需要進行執行緒同步。為了提高分配效率,JVM會為每個執行緒從eden區初始化一塊堆記憶體,該記憶體是執行緒私有的。這樣每次分配物件時就無需進行同步操作,從而提高物件分配效率。執行緒的這塊區域性記憶體區域被稱為執行緒區域性緩衝區(TLAB)。通常這塊記憶體會小於eden區的1%。當這塊記憶體用完時,就會重新通過CAS的方式為執行緒重新分配一塊TLAB。
    通常物件分配有兩種方式,一種是線性分配,當記憶體是規整時(大部分垃圾回收器新生代都是用標記清理演算法,可以保證記憶體規整),通過一個指標向後移動物件大小,直接分配一塊記憶體給物件,指標左邊是已使用的記憶體,指標右邊是未使用的記憶體,這種方式被稱為指標碰撞。TLAB配合指標碰撞技術能夠線上程安全的情況下移動一次指標直接就可以完成物件的記憶體分配。
    20210305092140.png
    當記憶體不規整時(比如CMS垃圾回收器通常情況並不會每次GC後都壓縮記憶體,會存在記憶體碎片),則需要一塊額外的記憶體記錄哪些記憶體是空閒的,這個快取被稱為空閒列表
    20210304174747.png

  • eden區分配
    如果TLAB無法分配物件,那麼物件只能在Eden區直接分配,前面說過,在堆上分配,必須採用同步策略避免有產生執行緒安全問題。如果分配記憶體時,物件的klass沒有解析過,則需要先進行類載入過程,然後才能分配物件。這個過程被稱為慢速分配,而如果klass已解析過則直接可以分配物件,這個過程被稱為快速分配

  • 老年代分配
    當eden區放不下物件時(當然還有其他的判斷策略,這裡暫時不去關心),物件直接分配到老年代。

  • 物件例項初始化
    當物件完成記憶體分配時,就會初始化物件,將記憶體清零。需要注意,物件的靜態變數在類初始化的初始化階段已經完成設定。

  • 初始化物件頭部
    當物件例項初始化完,就會設定物件頭部,預設的物件頭部存放在klass,如果啟用了偏向,則設定的就是可偏向的物件頭。

物件訪問方式

現在我們瞭解了物件的記憶體佈局和物件的建立邏輯,那麼物件在執行時,如何通過棧的區域性變數找到實際的物件呢?常用的物件訪問方式有2種,直接指標訪問控制程式碼訪問

直接指標訪問

物件建立時,區域性變數表只儲存物件的地址,地址指向的是堆中的實際物件的markword地址,JVM中採用的就是這種方式訪問物件。

20210304155643.png

控制程式碼訪問

通過控制程式碼訪問時區域性變數儲存的時控制程式碼池的物件控制程式碼,控制程式碼池中,則會儲存物件例項指標和物件型別指標。再通過這兩個指標分別指向物件例項池中的物件和後設資料的klass。

相比直接指標訪問,這種訪問方式由於需要2次訪問,而直接指標只需要一次訪問,因此控制程式碼訪問物件的速度相對較慢。但是對於垃圾回收器來說是比較友好的,因為物件移動無需更新棧中的區域性變數表的內容,只需要更新控制程式碼池中的物件例項指標的值。

20210304155847.png

HSDB

前面我們通過JOL工具可以很方便的輸出物件的佈局。JDK也提供了一些工具可以檢視更詳細的執行時資料。
HSDB(Hotspot Debugger) 是 JDK1.8 自帶的工具,使用該工具可以連線到執行時的java程式,檢視到JVM執行時的狀態。

以該偏向鎖程式碼為例

//-XX:BiasedLockingStartupDelay=0
public class BiasedLock {
    public static void main(String[] args) {
        Object o = new Object();
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}    
/*
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 28 64 03 (00000101 00101000 01100100 00000011) (56895493)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*/

為了能看到執行時狀態,我們可以使用idea工具單筆除錯,也可以使用jdb工具進行除錯。jdb是Java的偵錯程式,位於%JAVA_HOME%/bin下面。 通過jdb -classpath XXX class名 執行main方法。
執行後,我們可以將打斷點,然後進行除錯。

  • 通過stop in <class id>.<method>[(argument_type,...)]在方法中打斷點,或者可以通過stop at <class id>:<line>在指定行打斷點。
  • 通過stop in com.company.BiasedLock.main將斷點打在main方法。
  • 通過run執行
  • 通過next進行除錯。(可以使用step進行單步除錯)
C:\Users\Dm_ca>jdb -classpath "D:\study\java\symbolreference\target\classes;D:\develop\mavenrepository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar" com.company.lock.BiasedLock
正在初始化jdb...
> stop in com.company.lock.BiasedLock.main
正在延遲斷點com.company.lock.BiasedLock.main。
將在載入類後設定。
> run
執行com.company.lock.BiasedLock
設定未捕獲的java.lang.Throwable
設定延遲的未捕獲的java.lang.Throwable
>
VM 已啟動: 設定延遲的斷點com.company.lock.BiasedLock.main

斷點命中: "執行緒=main", com.company.lock.BiasedLock.main(), 行=8 bci=0

main[1] next
>
已完成的步驟: "執行緒=main", com.company.lock.BiasedLock.main(), 行=9 bci=8
...
未掛起任何物件。
> java.lang.Object o
bject internals:
 OFFSET已完成的步驟:   SIZE   TYPE DESCRIPTION                               VALUE
 "執行緒=main", com.company.lock.BiasedLock.main(), 行=11 bci=25
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

此時我們可以通過HSDB連線到程式。通過JPS 命令檢視程式的pid

C:\Users\Dm_ca>jps
17072
22724 HSDB
23268 TTY
24548 Launcher
3828 BiasedLock

通過java -cp “.;%JAVA_HOME%/lib/sa-jdi.jar” sun.jvm.hotspot.HSDB 3828 啟動HSDB(這種方式會阻塞我們的程式,不要直接在生產環境這樣操作)
第一次啟動可能會報錯誤無法找到sawindbg.dll,這時需要將%JAVA_HOME%/lib目錄下面的sawindbg.dll檔案拷貝到jre的/lib目錄下即可。

啟動後,在介面選中main執行緒,點選工具欄第二個圖片開啟執行緒棧。

20210304175143.png

HSDB工具線上程棧中已經標出我們的物件。在選單找到記憶體檢視器

20210304175304.png
輸入棧區域性變數表中的物件的地址,就可以顯示出物件的記憶體,和JOL工具列印的物件頭部是一樣的。
20210304175405.png

參考文件

  1. HSDB - HotSpot debugger
  2. JOL:分析Java物件的記憶體佈局
  3. [Java JVM] Hotspot GC研究- 開篇&物件記憶體佈局
  4. 看了這篇文章,我搞懂了StringTable
  5. 盤一盤 synchronized (一)—— 從列印Java物件頭說起
  6. 淺談偏向鎖、輕量級鎖、重量級鎖
  7. 原始碼解析-執行緒A請求偏向於執行緒B的偏向鎖
  8. C++為什麼要弄出虛表這個東西?
  9. 《深入理解Java虛擬機器》
  10. 《Java虛擬機器規範(Java SE 8版)》

20191127212134.png
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:https://www.cnblogs.com/Jack-Blog/p/14481982.html
作者:傑哥很忙
本文使用「CC BY 4.0」創作共享協議。歡迎轉載,請在明顯位置給出出處及連結。

相關文章