JVM---對象內存布局(jol插件驗證)


對象在內存中的布局

  1.對象頭

    mark word

    class pointer(有些地方寫作klass word)

    array length(如果常見的對象是數組則有這項,若不是,則不存在這一項)

  2.實例數據

  3.對齊填充

對象頭

在32位系統中,mark word占4個字節,class pointer占4個字節,因此對象頭共占8個字節

mark word

32位系統中

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |情況
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       | a.無鎖不可偏向(有hash),b.無鎖可偏向(無hash)
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       | 偏向鎖已偏向
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked | 輕量鎖
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked | 重量鎖
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   | GC標識
|-------------------------------------------------------|--------------------|

上述其實表示在鎖升級的時候,對象頭中存儲數據布局

 

biased_lock lock:2 狀態
0 01 無鎖
1 01 偏向鎖
  00 輕量級送
  10 重量級所
  11 GC標識

注:不能單純根據001和101來判斷時候加了偏向鎖,還應該看時候有偏向時的線程ID,101只是表示對象可偏向可以參看例子

age GC年齡,對象在Survivor區賦值一次,年齡加1,當達到設定閾值或者。。。時,將會晉升到老年代(對象進入老年代,具體請參考),由於只占4 bits所以最大值位15

identity_hashcode 對象標識嗎,調用System.identityHashCode()時,才會設置進入對象頭,對象沒有重寫hashcode方法,則默認使用該值;若對象重寫了hashcode方法,則對象頭中不保存該數據

thread 持有偏向鎖的線程ID

epoch 偏向時間戳

ptr_to_lock_record 指向棧中鎖記錄的指針

ptr_to_heavyweight_monitor 指向管程Monitor的指針

 

64位內存布局如下,標記為具體一次和32相同

|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|

class pointer 指向對象類型數據(這部分數據存儲在方法區中)的指針

在32位JVM中占32 bits,在64位JVM中占64 bits。在64位系統中會導致內存的浪費,JVM提供參數+UseCompressedClassPointers(我想看到這個名字,就應該會明白他的用法了吧)。當然,很多地方寫到使用+UseCompressedOops進行控制,其實這個地方的oop是指ordinary object pointer(普通對象指針),因此+UseCompressedOops其實比+UseCompressedClassPointers所包含的范圍廣

 

設置+UseCompressedOops,哪些信息會被壓縮?
1.對象的全局靜態變量(即類屬性)
2.對象頭信息:64位平台下,原生對象頭大小為16字節,壓縮后為12字節
3.對象的引用類型:64位平台下,引用類型本身大小為8字節,壓縮后為4字節
4.對象數組類型:64位平台下,數組類型本身大小為24字節,壓縮后16字節

哪些信息不會被壓縮?
1.指向非Heap的對象指針
2.局部變量、傳參、返回值、NULL指針
 

關閉普通對象指針壓縮,即-UseCompressedOops,則類型指針自動設置為-UseCompressedClassPointers,即使開啟類型指針,即設置+UseCompressedClassPointers,該參數也無效

開啟普通對象指針壓縮,即+UseCompressedOops,則類型指針自動設置為+UseCompressedClassPointers。此時可以設置-UseCompressedClassPointers,該參數設置有效

 

array length 對於數組對象,另外存了下數據的長度,32為JVM上,為32 bits,在64為JVM上,為64bits,開啟普通對象指針壓縮,即+UseCompressedOops,則占32 bits

 

實例數據

無論是自己定義的,還是從父類繼承的,都需要記錄下來。

 

對齊填充

並不是必須存在的,Hot Spot虛擬機自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。

 

 

例子驗證(說明,例子是我在64位JVM上運行的結果)

導入JAR文件

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
public class JavaObjectLayoutTest {
    public static void main(String[] args) {

        Object o = new Child();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        o.hashCode();
        System.out.println(o.hashCode());
        System.out.println("對應十六進制表示:"+Integer.toHexString(o.hashCode()));
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

class Parent {
    Long a =1l;
    long b =1;
}
class Child extends  Parent{

}

在vm中設置參數-XX:+PrintCommandLineFlags 

運行結果:

-XX:InitialHeapSize=265816960 -XX:MaxHeapSize=4253071360 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
Child object internals:
 OFFSET  SIZE             TYPE DESCRIPTION                               VALUE
      0     4                  (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4                  (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                  (object header)                           82 c1 00 f8 (10000010 11000001 00000000 11111000) (-134168190)
     12     4   java.lang.Long Parent.a                                  1
     16     8             long Parent.b                                  1
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

648129364
對應十六進制表示:26a1ab54
Child object internals:
 OFFSET  SIZE             TYPE DESCRIPTION                               VALUE
      0     4                  (object header)                           09 54 ab a1 (00001001 01010100 10101011 10100001) (-1582607351)
      4     4                  (object header)                           26 00 00 00 (00100110 00000000 00000000 00000000) (38)
      8     4                  (object header)                           82 c1 00 f8 (10000010 11000001 00000000 11111000) (-134168190)
     12     4   java.lang.Long Parent.a                                  1
     16     8             long Parent.b                                  1
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

查看打印結果,程序運行時默認開啟了對象指針壓縮和類型指針壓縮-XX:+UseCompressedOops  -XX:+UseCompressedClassPointers

新建child對象時,從父類繼承的實例對象也會占對象空間,其中根據是否為基本數據類型,基本類型占用字節請參考https://www.cnblogs.com/sxrtb/p/12294979.html引用類型在開啟指針壓縮(-XX:+UseCompressedOops)時,占用4個字節(關閉時占8個字節)

 

未使用hashcode方法時,對象的內存布局為

 此處mark word為 00000000_00000000_00000000_00100110_10100001_10101011_01010100_00001001,這個圖里看到的真好相反,這個涉及到“大端存儲和小端存儲”這個知識。我們這看到的倒着存,是應為(計算機這里)用的小端存儲

此時對象為新建狀態,代表鎖狀態的數據占3 bits,為mark word中最后3 bits,為001,

依次,0001表示GC年齡,這個地方我剛剛新建,怎么就有GC呢,這個運行結果說明我這邊的進行過GC操作(自己做測試,可能不是0001,這個是否進行過GC,可以通過-verbose:gc或者加上-XX:+PrintGCDetails就可以觀察到,后續JVM中我也會寫道具體GC參數說明)

 

使用過hashcode(System.identityHashCode())后,打印hashcode和其對應的十六進制整數,對象的內存布局為

 在64為JVM中,hashcode占31 bits,順着上面的凡是,則0100110 10100001 10101011 01010100表示hashcode,通過圖示,也可以看到這數據用十六進制表示,也剛好是26a1ab54

 

同理,若位數組對象

public class JavaObjectLayoutTest {
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new B[3]).toPrintable());
    }
}

class B{

}
-XX:InitialHeapSize=265816960 -XX:MaxHeapSize=4253071360 -XX:+PrintCommandLineFlags -XX:+UseBiasedLocking -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
[LB; object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           81 c1 00 f8 (10000001 11000001 00000000 11111000) (-134168191)
     12     4        (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
     16    12      B [LB;.<elements>                           N/A
     28     4        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

對照着上面的分析,我就不過多說明了。

 

一般看別人的例子,都會給對象加鎖,如synchronized(o),此時對象都的信息也會改變

public class JavaObjectLayoutTest {
    public static void main(String[] args) {

        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}
-XX:InitialHeapSize=265816960 -XX:MaxHeapSize=4253071360 -XX:+PrintCommandLineFlags -XX:+UseBiasedLocking -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      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

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           b8 f5 1d 03 (10111000 11110101 00011101 00000011) (52295096)
      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

從運行結果上來看,這個地方直接加了一個輕量級鎖。

 

提問:

1.如果我們這個時候使用hashcode()方法,這個肯定可以打印出來,但是根據對象頭信息布局,此時hashcode的位置已經被占用了,那這個答應出來的hashcode時從哪里來的呢?

2.為什么直接加的是輕量級鎖,而不是偏向鎖

我這個地方主要寫的時對象的內存布局,關於鎖的知識我后續會詳細寫,答案引用別人的博客

可以參考https://blog.csdn.net/P19777/article/details/103125545

 這個例子也說明了001101不能直接判斷是否加了偏向鎖

public class A {
}
public class JavaObjectLayoutTest {
    public static void main(String[] args) throws Exception {
        // 需要sleep一段時間,因為java對於偏向鎖的啟動是在啟動幾秒之后才激活。
        // 因為jvm啟動的過程中會有大量的同步塊,且這些同步塊都有競爭,如果一啟動就啟動
        // 偏向鎖,會出現很多沒有必要的鎖撤銷
        Thread.sleep(5000);
        A a = new A();
        // 未出現任何獲取鎖的時候
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        synchronized (a){
            // 獲取一次鎖之后
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }
        // 輸出hashcode
        System.out.println(a.hashCode());
        // 計算了hashcode之后
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        synchronized (a){
            // 再次獲取鎖
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    }
}
-XX:InitialHeapSize=265816960 -XX:MaxHeapSize=4253071360 -XX:+PrintCommandLineFlags -XX:+UseBiasedLocking -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           0d 00 00 00 (00001101 00000000 00000000 00000000) (13)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           0d 28 1b 03 (00001101 00101000 00011011 00000011) (52111373)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

565760380
A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           09 7c d1 b8 (00001001 01111100 11010001 10111000) (-1194230775)
      4     4        (object header)                           21 00 00 00 (00100001 00000000 00000000 00000000) (33)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           a0 f3 07 03 (10100000 11110011 00000111 00000011) (50852768)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

 運行結果為什么會這樣,可以參考下面(只是參考)

另外補充,對對象加鎖(在開啟偏向鎖的時候,默認開啟,關閉-XX:-UseBiasedLocking),對象沒使用hashcode,則鎖解除后鎖標志位為101;若重寫hashcode()方法,且調用hashcode(),則鎖解除后鎖標志位為101;若沒有重寫hashcode()方法,且調用hashcode(),則鎖解除后鎖標志位為001;

 

注意:若使用-XX:+UseBiasedLocking,剛開始啟動JVM時,實際上-XX:+UseBiasedLocking暫時還沒有生效,這個時候創建得對象鎖標記位為001,這個時不可偏向的,升級時只能升級為輕量鎖;再使用輕量級鎖的時候,當前棧幀中建立一個名為Lock Record的控件,用戶存儲鎖對象的Mark Word的拷貝,使用CAS操作嘗試將Mark Word更新為Lock Record的指針,若成功,則標識這個對象擁有了該對象的鎖(此次出hashcode,若Lock Record中有hashcode,則輕量級鎖不變;若無,變成重量級鎖);

  若-XX:+UseBiasedLocking徹底生效后,創建的對象的鎖標記位置為101,若有一個線程需要加鎖時,這個鎖為偏向鎖(若此時使用hashcode,此時Mark Word中不可能有hashcode,則鎖直接升級為重量級鎖。若是再使用偏向鎖之前,使用hascode,則鎖標記為001,標識不可偏向,則后續直接添加輕量級鎖)

  使用hashcode的時,若數據未加鎖,則此時鎖標記位置未001,標識不可偏向。若對象再鎖的時候使用hashcode,而hashcode由不從得知,則需要進行鎖升級。

  鎖標記位置未10,也不代表一定添加的重量級鎖。

 

 

 

引用怎樣訪問(找到)對象

  句柄

  直接指針(Hot Spot使用這種方式)

優點:reference存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要改變。

缺點:增加了一次指針定位的時間開銷。

 

優點:節省了一次指針定位的開銷。

缺點:在對象被移動時reference本身需要被修改。

 

 

寫到這里就結束了,這里主要寫的時對象的內存布局,如果要了解鎖升級的過程,可以參考https://www.cnblogs.com/ZoHy/p/11313155.html

 

例題,分析寫new A()和new B()所占用的空間

static class A{
    String s = new String();
    int i = 0;
}

static class B{
    String s;
    int i;
}

 

常見對象占用空間(在64為中開啟指針壓縮)

 

 

參考:https://www.jianshu.com/p/3d38cba67f8b

https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/hotspot/share/oops/markOop.hpp

https://www.jianshu.com/p/8580ab50e261

https://blog.csdn.net/P19777/article/details/103125545

 https://www.cnblogs.com/ZoHy/p/11313155.html

https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/hotspot/share/oops/markOop.hpp

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM