java物件的記憶體佈局

2023-07-03 12:00:25

寫java時不管是我們自己new物件還是spring管理bean,儘管我們天天跟物件打交道,那麼物件的結構和記憶體佈局有多少人知道呢,這篇文章可帶你入門,瞭解java物件記憶體佈局。
本文涉及到JVM指標壓縮的知識點,不熟悉的小夥伴可以看前面寫過的一篇關於指標壓縮的文章。
JVM之指標壓縮

首先說明,本文涉及的JDK版本是1.8,JVM虛擬機器器是64位元的HotSpot實現為準。

java物件結構

關於java物件我們知道, 物件的範例是存在堆上的, 物件的後設資料存在方法區(元空間)上,物件的參照儲存在棧上的。那麼java物件的結構是什麼樣的呢,其實java物件由三部分構成。

  1. 物件頭

物件頭裡也有三部分構成。

  • Markword

儲存物件的hashCode、垃圾回收物件的年齡以及鎖資訊等。

  • 型別指標

物件指向的類資訊地址即後設資料指標,比如User物件指標指向User.class的JVM記憶體地址。注意:jdk1.8以後後設資料是存在Metaspace裡的,jdk1.8之前是在方法區裡

  • 陣列長度

只有物件是陣列的情況下,才有這部分資料,若物件不是陣列,則沒有這部分,不分配空間。

  1. 物件體

物件裡的非靜態屬性佔用的空間(包括父類別的所有屬性,不區分修飾型別),不包括方法,注意:是非靜態屬性,屬於物件的屬性,靜態屬性是屬於類的不在物件上分配空間。如果屬性是基本資料型別,則直接存物件本身,如果是參照型別,則存的是物件的指標。

  1. 對齊填充

預設情況下,如果物件頭+物件體大小不是8位元組的倍數,則通過該部分進行補齊,比如物件頭+物件體大小隻有30位元組,則需要補齊到32位元組,這裡的對齊填充就是2位元組。預設情況下,JVM中物件是以8位元組對齊的,若物件頭加上物件體是8的倍數時,則不存在位元組對齊,否則會填充補齊到8的倍數。
物件結構如下圖所示。

通過圖中可以看出,陣列物件只是在物件頭裡多了陣列長度這一項,普通物件(非陣列物件)沒有這項,也不分配記憶體空間。
物件結構及佔用空間大小如下圖所示。

涉及指標壓縮的地方有兩個,一個是物件頭裡的型別指標,一個是物件體裡的參照型別指標,這篇文章裡有詳細的介紹:JVM之指標壓縮

物件頭

物件頭包含三部分

  • Markword:儲存物件自身執行時資料如hashcode、gc分代年齡及鎖資訊等,64位元系統總共佔用8個位元組。
  • 型別指標:物件指向類後設資料地址的指標,jdk8預設開啟指標壓縮,64位元系統佔4個位元組
  • 陣列長度:若物件不是陣列,則沒有該部分,不分配空間大小,若是陣列,則為4個位元組長度

物件頭佔用空間大小如下表所示。

Markword

儲存物件自身執行時資料如hashcode、gc分代年齡及鎖資訊等,64位元系統總共佔用8個位元組,也就是64bit,64位元的二進位制0和1。

解釋如下:

  • 物件的hashCode佔31位元,重寫類的hashCode方法返回int型別,只有在無鎖情況下,是在有呼叫的情況下會計算該值並寫到物件頭中,其他情況該值是空的。
  • 分代年齡佔4位元,最大值也就是15,在GC中,當survivor區中物件複製一次,年齡加1,預設是到15之後會移動到老年代。
  • 是否偏向鎖佔1位,無鎖和偏向鎖的最後兩位都是01,使用這一位來標識區分是無鎖還是偏向鎖。
  • 鎖標誌位佔2位,鎖狀態標記位,同是否偏向鎖標誌位標識物件處於什麼鎖狀態。
  • 偏向執行緒ID佔54位元,只有偏向鎖狀態才有,這個ID是作業系統層面的執行緒唯一id,跟java中的執行緒id是不一致的。

型別指標

型別指標指向類的後設資料地址,JVM通過這個指標確定物件是哪個類的範例。32位元的JVM佔32位元,4個位元組,64位元的JVM佔64位元,8個位元組,但是64位元的JVM預設會開啟指標壓縮,壓縮後也只佔4位元組。
64位元虛擬機器器中在堆記憶體小於32GB的情況下,UseCompressedOops是預設開啟的,該參數列示開啟指標壓縮,會將原來64位元的指標壓縮為32位元。
-XX:+UseCompressedClassPointers //開啟壓縮類指標
-XX:-UseCompressedClassPointers //關閉壓縮類指標

這個JVM引數依賴UseCompressedOops這個引數,UseCompressedOops開啟,UseCompressedClassPointers預設開啟,可手工關閉,UseCompressedOops關閉,UseCompressedClassPointers不管開啟還是關閉都不生效即不壓縮。

陣列長度

如果物件是普通物件非陣列物件,則沒有這部分,不佔用空間。
如果物件是一個陣列,則將陣列的長度存到物件頭裡,表示陣列的大小。

物件體

物件體裡放的是非靜態的屬性,也包括父類別的所有非靜態屬性(private修飾的也在這裡,不區分可見性修飾符),基本型別的屬性存放的是具體的值,參照型別及陣列型別存放的是參照指標。

對齊填充

虛擬機器器為了高效定址,採用8位元組對齊,所以物件大小不是8的倍數時,會補齊對應的位置,比如物件頭+物件體是32位元組時,則不需要對齊填充,物件頭+物件體是12位元組時,則需補齊4位元。

物件大小的計算

物件的大小跟指標壓縮是否開啟有關,可通過以下兩個引數控制。
UseCompressedClassPointers:壓縮類指標(開啟時類指標占4位元組,關閉時類指標占8位元組)
UseCompressedOops:壓縮普通物件指標(開啟時參照物件指標占4位元組,關閉時參照物件指標占8位元組)
這兩個引數預設是開啟的,即-XX:+UseCompressedClassPointers,-XX:+UseCompressedOops,也可手動設定,如下所示

-XX:+UseCompressedClassPointers //開啟壓縮類指標
-XX:-UseCompressedClassPointers //關閉壓縮類指標
-XX:+UseCompressedOops  //開啟壓縮普通物件指標
-XX:-UseCompressedOops  //關閉壓縮普通物件指標

32位元HotSpot VM是不支援UseCompressedOops引數的,只有64位元HotSpot VM才支援。
Oracle JDK從6 update 23開始在64位元系統上會預設開啟壓縮指標。

以下表格展示了物件中各部分所佔空間大小,單位:位元組。

型別 所屬部分 佔用空間大小(壓縮開啟) 佔用空間大小(壓縮關閉)
Markwork 物件頭 8 8
型別指標 物件頭 4 8
陣列長度 物件頭 4 4
byte 物件體 1 1
boolean 物件體 1 1
short 物件體 2 2
char 物件體 2 2
int 物件體 4 4
float 物件體 4 4
long 物件體 8 8
double 物件體 8 8
物件參照指標 物件體 4 8
對齊填充 對齊填充 物件頭+物件體是8的倍數?0 :8 -(物件頭+物件體)% 8 物件頭+物件體是8的倍數?0 :8 -(物件頭+物件體)% 8

物件大小計算公式
物件大小=物件頭 + 物件體(物件是陣列時,物件體的大小=參照指標占用空間大小*物件個數) + 對齊填充
64位元作業系統32G記憶體以下,預設開啟物件指標壓縮,物件頭是12位元組,關閉指標壓縮,物件頭是16位元組。記憶體超過32G時,則自動關閉指標壓縮,物件頭佔16位元組。

物件分析

有了以上的理論知識,我們通過實際案例進行物件分析。
使用 JOL 工具分析 Java 物件大小
maven依賴

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

常用類及方法
檢視物件內部資訊: ClassLayout.parseInstance(obj).toPrintable()
檢視物件外部資訊:GraphLayout.parseInstance(obj).toPrintable()
檢視物件佔用空間總大小:GraphLayout.parseInstance(obj).totalSize()
檢視類內部資訊:ClassLayout.parseClass(Object.class).toPrintable()

使用到的測試類:

@Setter
class Goods {
    private byte b;
    private char type;
    private short age;
    private int no;
    private float weight;
    private double price;
    private long id;
    private boolean flag;
    private String goodsName;
    private LocalDateTime produceTime;
    private String[] tags;
    public static String str;
    public static int temp;
}

非陣列物件,開啟指標壓縮

64位元JVM,堆記憶體小於32G的情況下,預設是開啟指標壓縮的。

public static void main(String[] args) {
    Goods goods = new Goods();
    goods.setAge((short) 10);
    goods.setNo(123456);
    goods.setId(111L);
    goods.setGoodsName("方便麵");
    goods.setFlag(true);
    goods.setB((byte)1);
    goods.setPrice(1.5d);
    goods.setProduceTime(LocalDateTime.now());
    goods.setType('A');
    goods.setWeight(0.065f);
    goods.setTags(new String[] {"food", "convenience", "cheap"});
    Goods.str = "test";
    Goods.temp = 222;
    System.out.println(ClassLayout.parseInstance(goods).toPrintable());
}

計算物件大小:
先不看輸出結果,按上面的公式計算一下物件的大小:
物件頭:8位元組(Markword)+4位元組(類指標)=12位元組
物件體:1位元組(屬性b)+ 2位元組(屬性type)+ 2位元組(屬性age)+ 4位元組(屬性no)+ 4位元組(屬性weight)+ 8位元組(屬性price)+ 8位元組(屬性id)+ 1位元組(屬性flag) + 4位元組(屬性goodsName指標) + 4位元組(屬性produceTime指標) + 4位元組(屬性tags指標)= 42位元組(注意:靜態屬性不參與物件大小計算)
對齊填充:8 -(物件頭+物件體)% 8 = 8 - (12 + 42) % 8 = 2位元組
物件大小=物件頭 + 物件體 + 對齊填充 = 12位元組 + 42位元組 + 2位元組 = 56位元組。
執行看執行結果:

com.star95.study.jvm.Goods object internals:
OFF  SZ                      TYPE DESCRIPTION               VALUE
  0   8                           (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4                           (object header: class)    0x2000c043
 12   4                       int Goods.no                  123456
 16   8                    double Goods.price               1.5
 24   8                      long Goods.id                  111
 32   4                     float Goods.weight              0.065
 36   2                      char Goods.type                A
 38   2                     short Goods.age                 10
 40   1                      byte Goods.b                   1
 41   1                   boolean Goods.flag                true
 42   2                           (alignment/padding gap)   
 44   4          java.lang.String Goods.goodsName           (object)
 48   4   java.time.LocalDateTime Goods.produceTime         (object)
 52   4        java.lang.String[] Goods.tags                [(object), (object), (object)]
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total

這裡有一個特殊的地方,列印輸出的屬性順序跟程式碼裡的順序不一致,這是因為JVM進行優化,也就是指令重排序,會根據屬性型別的大小、執行的先後順序對結果是否有影響、最小填充大小等因素計算出物件最小應占用的空間。

非陣列物件,關閉指標壓縮

計算物件大小:
關閉壓縮指標,類指標和參照物件指標都佔8位元組,推算一下物件大小:
物件頭:8位元組(Markword)+8位元組(類指標)=16位元組
物件體:1位元組(屬性b)+ 2位元組(屬性type)+ 2位元組(屬性age)+ 4位元組(屬性no)+ 4位元組(屬性weight)+ 8位元組(屬性price)+ 8位元組(屬性id)+ 1位元組(屬性flag) + 8位元組(屬性goodsName指標) + 8位元組(屬性produceTime指標) + 8位元組(屬性tags指標)= 54位元組(注意:靜態屬性不參與物件大小計算)
對齊填充:8 -(物件頭+物件體)% 8 = 8 - (16 + 54) % 8 = 2位元組
物件大小=物件頭 + 物件體 + 對齊填充 = 16位元組 + 54位元組 + 2位元組 = 72位元組。
執行時增加JVM引數如下:

-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class ObjectLayOut1 {
    public static void main(String[] args) {
        Goods goods = new Goods();
        goods.setAge((short) 10);
        goods.setNo(123456);
        goods.setId(111L);
        goods.setGoodsName("方便麵");
        goods.setFlag(true);
        goods.setB((byte)1);
        goods.setPrice(1.5d);
        goods.setProduceTime(LocalDateTime.now());
        goods.setType('A');
        goods.setWeight(0.065f);
        goods.setTags(new String[] {"food", "convenience", "cheap"});
        Goods.str = "test";
        Goods.temp = 222;
        System.out.println(ClassLayout.parseInstance(goods).toPrintable());
    }
}

執行看執行結果:

com.star95.study.jvm.Goods object internals:
OFF  SZ                      TYPE DESCRIPTION               VALUE
  0   8                           (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8                           (object header: class)    0x00000000175647b8
 16   8                    double Goods.price               1.5
 24   8                      long Goods.id                  111
 32   4                       int Goods.no                  123456
 36   4                     float Goods.weight              0.065
 40   2                      char Goods.type                A
 42   2                     short Goods.age                 10
 44   1                      byte Goods.b                   1
 45   1                   boolean Goods.flag                true
 46   2                           (alignment/padding gap)   
 48   8          java.lang.String Goods.goodsName           (object)
 56   8   java.time.LocalDateTime Goods.produceTime         (object)
 64   8        java.lang.String[] Goods.tags                [(object), (object), (object)]
Instance size: 72 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total

陣列物件開啟指標壓縮

計算物件大小:
預設是開啟壓縮指標的,類指標和參照物件指標都佔4位元組,推算一下物件大小:
物件頭:8位元組(Markword)+ 4位元組(類指標) + 4位元組(陣列長度)= 16位元組
物件體:4位元組 * 3 = 12位元組
對齊填充:8 -(物件頭+物件體)% 8 = 8 - (16位元組 + 12位元組)% 8= 4位元組
物件大小=物件頭 + 物件體 + 對齊填充 = 16位元組 + 12位元組 + 4位元組 = 32位元組。

public class ObjectLayOut1 {
    public static void main(String[] args) {
        Goods goods = new Goods();
        goods.setAge((short) 10);
        goods.setNo(123456);
        goods.setId(111L);
        goods.setGoodsName("方便麵");
        goods.setFlag(true);
        goods.setB((byte)1);
        goods.setPrice(1.5d);
        goods.setProduceTime(LocalDateTime.now());
        goods.setType('A');
        goods.setWeight(0.065f);
        goods.setTags(new String[] {"food", "convenience", "cheap"});
        Goods.str = "test";
        Goods.temp = 222;
        Goods[] goodsArr = new Goods[3];
        goodsArr[0] = goods;
        System.out.println(ClassLayout.parseInstance(goodsArr).toPrintable());
    }
}

執行看執行結果:

[Lcom.star95.study.jvm.Goods; object internals:
OFF  SZ                         TYPE DESCRIPTION               VALUE
  0   8                              (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4                              (object header: class)    0x2000c18d
 12   4                              (array length)            3
 16  12   com.star95.study.jvm.Goods Goods;.<elements>         N/A
 28   4                              (object alignment gap)    
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

陣列物件關閉指標壓縮

計算物件大小:
關閉壓縮指標,類指標和參照物件指標都佔8位元組,推算一下物件大小:
物件頭:8位元組(Markword)+8位元組(類指標) + 4位元組(陣列長度)=20位元組
物件體:8位元組 * 3 = 24位元組
對齊填充:8 -(物件頭+物件體)% 8 = 8 - (20+ 24) % 8 = 4位元組
物件大小=物件頭 + 物件體 + 對齊填充 = 20位元組 + 24位元組 + 4位元組 = 48位元組。
執行時增加JVM引數如下:

-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class ObjectLayOut1 {
    public static void main(String[] args) {
        Goods goods = new Goods();
        goods.setAge((short) 10);
        goods.setNo(123456);
        goods.setId(111L);
        goods.setGoodsName("方便麵");
        goods.setFlag(true);
        goods.setB((byte)1);
        goods.setPrice(1.5d);
        goods.setProduceTime(LocalDateTime.now());
        goods.setType('A');
        goods.setWeight(0.065f);
        goods.setTags(new String[] {"food", "convenience", "cheap"});
        Goods.str = "test";
        Goods.temp = 222;
        Goods[] goodsArr = new Goods[3];
        goodsArr[0] = goods;
        System.out.println(ClassLayout.parseInstance(goodsArr).toPrintable());
    }
}

執行看執行結果:

[Lcom.star95.study.jvm.Goods; object internals:
OFF  SZ                         TYPE DESCRIPTION               VALUE
  0   8                              (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   8                              (object header: class)    0x0000000017e04d70
 16   4                              (array length)            3
 20   4                              (alignment/padding gap)   
 24  24   com.star95.study.jvm.Goods Goods;.<elements>         N/A
Instance size: 48 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

通過以上物件分析,我們看到在開啟壓縮指標的情況下,物件的大小會小很多,節省了記憶體空間。

總結

通過以上的分析,基本已經把java物件的結構講清楚了,另外物件佔用記憶體空間大小也計算出來,有助於進行JVM調優分析,64位元的虛擬機器器記憶體在32G以下時預設是開啟壓縮指標的,超過32G自動關閉壓縮指標,主要目的都是為了提高定址效率。
另外,本文是通過JOL工具計算物件佔用空間的大小,不包括參照物件實際佔用的記憶體大小,因為計算時是按參照物件的指標占用空間大小計算的,可能跟其他工具計算的結果不一樣,具體跟工具的計算邏輯有關,比如跟JDK自帶的jvisualvm工具通過堆dump出來看到的物件大小不一樣,感興趣的可自行驗證。