Java對象內存布局

博客鏈接:http://www.ideabuffer.cn/2017/05/06/Java對象內存布局/


我們知道在Java中基本數據類型的大小,例如int類型占4個字節、long類型占8個字節,那么Integer對象和Long對象會占用多少內存呢?本文介紹一下Java對象在堆中的內存結構以及對象大小的計算。

對象的內存布局

一個Java對象在內存中包括對象頭、實例數據和補齊填充3個部分:

對象頭

  • Mark Word:包含一系列的標記位,比如輕量級鎖的標記位,偏向鎖標記位等等。在32位系統占4字節,在64位系統中占8字節;
  • Class Pointer:用來指向對象對應的Class對象(其對應的元數據對象)的內存地址。在32位系統占4字節,在64位系統中占8字節;
  • Length:如果是數組對象,還有一個保存數組長度的空間,占4個字節;

對象實際數據

對象實際數據包括了對象的所有成員變量,其大小由各個成員變量的大小決定,比如:byte和boolean是1個字節,short和char是2個字節,int和float是4個字節,long和double是8個字節,reference是4個字節(64位系統中是8個字節)。

Primitive Type Memory Required(bytes)
boolean 1
byte 1
short 2
char 2
int 4
float 4
long 8
double 8

對于reference類型來說,在32位系統上占用4bytes, 在64位系統上占用8bytes。

對齊填充

Java對象占用空間是8字節對齊的,即所有Java對象占用bytes數必須是8的倍數。例如,一個包含兩個屬性的對象:int和byte,這個對象需要占用8+4+1=13個字節,這時就需要加上大小為3字節的padding進行8字節對齊,最終占用大小為16個字節。

注意:以上對64位操作系統的描述是未開啟指針壓縮的情況,關于指針壓縮會在下文中介紹。

對象頭占用空間大小

這里說明一下32位系統和64位系統中對象所占用內存空間的大小:

  • 在32位系統下,存放Class Pointer的空間大小是4字節,MarkWord是4字節,對象頭為8字節;
  • 在64位系統下,存放Class Pointer的空間大小是8字節,MarkWord是8字節,對象頭為16字節;
  • 64位開啟指針壓縮的情況下,存放Class Pointer的空間大小是4字節,MarkWord是8字節,對象頭為12字節;
  • 如果是數組對象,對象頭的大小為:數組對象頭8字節+數組長度4字節+對齊4字節=16字節。其中對象引用占4字節(未開啟指針壓縮的64位為8字節),數組MarkWord為4字節(64位未開啟指針壓縮的為8字節);
  • 靜態屬性不算在對象大小內。

指針壓縮

從上文的分析中可以看到,64位JVM消耗的內存會比32位的要多大約1.5倍,這是因為對象指針在64位JVM下有更寬的尋址。對于那些將要從32位平臺移植到64位的應用來說,平白無辜多了1/2的內存占用,這是開發者不愿意看到的。

從JDK 1.6 update14開始,64位的JVM正式支持了 -XX:+UseCompressedOops 這個可以壓縮指針,起到節約內存占用的新參數。

什么是OOP?

OOP的全稱為:Ordinary Object Pointer,就是普通對象指針。啟用CompressOops后,會壓縮的對象:

  • 每個Class的屬性指針(靜態成員變量);
  • 每個對象的屬性指針;
  • 普通對象數組的每個元素指針。

當然,壓縮也不是所有的指針都會壓縮,對一些特殊類型的指針,JVM是不會優化的,例如指向PermGen的Class對象指針、本地變量、堆棧元素、入參、返回值和NULL指針不會被壓縮。

啟用指針壓縮

在Java程序啟動時增加JVM參數:-XX:+UseCompressedOops來啟用。

注意:32位HotSpot VM是不支持UseCompressedOops參數的,只有64位HotSpot VM才支持。

本文中使用的是JDK 1.8,默認該參數就是開啟的。

查看對象的大小

接下來我們使用http://www.javamex.com/中提供的classmexer.jar來計算對象的大小。

運行環境:JDK 1.8,Java HotSpot(TM) 64-Bit Server VM

基本數據類型

對于基本數據類型來說,是比較簡單的,因為我們已經知道每個基本數據類型的大小。代碼如下:

/**
 * VM options:
 * -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
 * -XX:+UseCompressedOops
 */
public class TestObjectSize {


    int a;
    long b;
    static int c;

    public static void main(String[] args) throws IOException {
        TestObjectSize testObjectSize = new TestObjectSize();
        // 打印對象的shallow size
        System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
        // 打印對象的 retained size
        System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
        System.in.read();
    }
}

注意:在運行前需要設置javaagent參數,在JVM啟動參數中添加-javaagent:/path_to_agent/classmexer.jar來運行。

有關Shallow Size和Retained Size請參考http://blog.csdn.net/e5945/article/details/7708253

開啟指針壓縮的情況

運行查看結果:

Shallow Size: 24 bytes
Retained Size: 24 bytes

根據上文的分析可以知道,64位開啟指針壓縮的情況下:

  • 對象頭大小=Class Pointer的空間大小為4字節+MarkWord為8字節=12字節;
  • 實際數據大小=int類型4字節+long類型8字節=12字節(靜態變量不在計算范圍之內)

在MAT中分析的結果如下:

所以大小是24字節。其實這里并沒有padding,因為正好是24字節。如果我們把long b;換成int b;之后,再來看一下結果:

Shallow Size: 24 bytes
Retained Size: 24 bytes

大小并沒有變化,說明這里做了padding,并且padding的大小是4字節。

這里的Shallow Size和Retained Size是一樣的,因為都是基本數據類型。

關閉指針壓縮的情況

如果要關閉指針壓縮,在JVM參數中添加-XX:-UseCompressedOops來關閉,再運行上述代碼查看結果:

Shallow Size: 24 bytes
Retained Size: 24 bytes

分析一下在64位未開啟指針壓縮的情況下:

  • 對象頭大小=Class Pointer的空間大小為8字節+MarkWord為8字節=16字節;
  • 實際數據大小=int類型4字節+long類型8字節=12字節(靜態變量不在計算范圍之內);

這里計算后大小為16+12=28字節,這時候就需要padding來補齊了,所以padding為4字節,最后的大小就是32字節。

我們再把long b;換成int b;之后呢?通過上面的計算結果可以知道,實際數據大小就應該是int類型4字節+int類型4字節=8字節,對象頭大小為16字節,那么不需要做padding,對象的大小為24字節:

Shallow Size: 24 bytes
Retained Size: 24 bytes

數組類型

64位系統中,數組對象的對象頭占用24 bytes,啟用壓縮后占用16字節。比普通對象占用內存多是因為需要額外的空間存儲數組的長度。基礎數據類型數組占用的空間包括數組對象頭以及基礎數據類型數據占用的內存空間。由于對象數組中存放的是對象的引用,所以數組對象的Shallow Size=數組對象頭+length * 引用指針大小,Retained Size=Shallow Size+length*每個元素的Retained Size。

代碼如下:

/**
 * VM options:
 * -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
 * -XX:+UseCompressedOops
 */
public class TestObjectSize {


    long[] arr = new long[6];

    public static void main(String[] args) throws IOException {
        TestObjectSize testObjectSize = new TestObjectSize();
        // 打印對象的shallow size
        System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
        // 打印對象的 retained size
        System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
        System.in.read();
    }
}

開啟指針壓縮的情況

結果如下:

Shallow Size: 16 bytes
Retained Size: 80 bytes

Shallow Size比較簡單,這里對象頭大小為12字節, 實際數據大小為4字節,所以Shallow Size為16。

對于Retained Size來說,要計算數組占用的大小,對于數組來說,它的對象頭部多了一個用來存儲數組長度的空間,該空間大小為4字節,所以數組對象的大小=引用對象頭大小12字節+存儲數組長度的空間大小4字節+數組的長度數組中對象的Retained Size+padding大小*

下面分析一下上述代碼中的long[] arr = new long[6];,它是一個長度為6的long類型的數組,由于long類型的大小為8字節,所以數組中的實際數據是68=48字節,那么數組對象的大小=12+4+68+0=64,最終的Retained Size=Shallow Size + 數組對象大小=16+64=80。

通過MAT查看如下:

關閉指針壓縮的情況

結果如下:

Shallow Size: 24 bytes
Retained Size: 96 bytes

這個結果大家應該能自己分析出來了,因為這時引用對象頭為16字節,那么數組的大小=16+4+6*8+4=72,(這里最后一個4是padding),所以Retained Size=Shallow Size + 數組對象大小=24+72=96。

通過MAT查看如下:

包裝類型

包裝類(Boolean/Byte/Short/Character/Integer/Long/Double/Float)占用內存的大小等于對象頭大小加上底層基礎數據類型的大小。

包裝類型的Retained Size占用情況如下:

Numberic Wrappers +useCompressedOops -useCompressedOops
Byte, Boolean 16 bytes 24 bytes
Short, Character 16 bytes 24 bytes
Integer, Float 16 bytes 24 bytes
Long, Double 24 bytes 24 bytes

代碼如下:

/**
 * VM options:
 * -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
 * -XX:+UseCompressedOops
 */
public class TestObjectSize {


    Boolean a = new Boolean(false);
    Byte b = new Byte("1");
    Short c = new Short("1");
    Character d = new Character('a');
    Integer e = new Integer(1);
    Float f = new Float(2.5);
    Long g = new Long(123L);
    Double h = new Double(2.5D);

    public static void main(String[] args) throws IOException {
        TestObjectSize testObjectSize = new TestObjectSize();
        // 打印對象的shallow size
        System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
        // 打印對象的 retained size
        System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
        System.in.read();
    }
}

開啟指針壓縮的情況

結果如下:

Shallow Size: 48 bytes
Retained Size: 192 bytes

MAT中的結果如下:

關閉指針壓縮的情況

結果如下:

Shallow Size: 80 bytes
Retained Size: 272 bytes

MAT中的結果如下:

String類型

在JDK1.7及以上版本中,java.lang.String中包含2個屬性,一個用于存放字符串數據的char[], 一個int類型的hashcode, 部分源代碼如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    ...
}

因此,在關閉指針壓縮時,一個String對象的大小為:

  • Shallow Size=對象頭大小16字節+int類型大小4字節+數組引用大小8字節+padding4字節=32字節

  • Retained Size=Shallow Size+char數組的Retained Size

在開啟指針壓縮時,一個String對象的大小為:

  • Shallow Size=對象頭大小12字節+int類型大小4字節+數組引用大小4字節+padding4字節=24字節

  • Retained Size=Shallow Size+char數組的Retained Size

代碼如下:

/**
 * VM options:
 * -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
 * -XX:+UseCompressedOops
 */
public class TestObjectSize {


    String s = "test";

    public static void main(String[] args) throws IOException {
        TestObjectSize testObjectSize = new TestObjectSize();
        // 打印對象的shallow size
        System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
        // 打印對象的 retained size
        System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
        System.in.read();
    }
}

開啟指針壓縮的情況

結果如下:

Shallow Size: 16 bytes
Retained Size: 64 bytes

MAT中的結果如下:

關閉指針壓縮的情況

結果如下:

Shallow Size: 24 bytes
Retained Size: 88 bytes

MAT中的結果如下:

其他引用類型的大小

根據上面的分析,可以計算出一個對象在內存中的占用空間大小情況,其他的引用類型可以參考分析計算過程來計算內存的占用情況。

關于padding

思考這樣一個問題,是不是padding都加到對象的后面呢,如果對象頭占12個字節,對象中只有1個long類型的變量,那么該long類型的變量的偏移起始地址是在12嗎?用下面一段代碼測試一下:

@SuppressWarnings("ALL")
public class PaddingTest {

    long a;

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println(UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("a")));
    }

}

這里使用Unsafe類來查看變量的偏移地址,運行后結果如下:

16

如果是換成int類型的變量呢?結果是12。

現在一般的CPU一次直接操作的數據可以到64位,也就是8個字節,那么字長就是64,而long類型本身就是占64位,如果這時偏移地址是12,那么需要分兩次讀取該數據,而如果偏移地址從16開始只需要通過一次讀取即可。int類型的數據占用4個字節,所以可以從12開始。

把上面的代碼修改一下:

@SuppressWarnings("ALL")
public class PaddingTest {

    long a;

    byte b;

    byte c;

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println(UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("a")));
        System.out.println(UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("b")));
        System.out.println(UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("c")));
    }

}

運行結果如下:

16
12
13

在本例中,如果變量的大小小于等于4個字節,那么在分配內存的時候會先優先分配,因為這樣可以減少padding,比如這里的b和c變量;如果這時達到了16個字節,那么其他的變量按照類型所占內存的大小降序分配。

再次修改代碼:

/**
 * VM options: -javaagent:D:\source-files\classmexer.jar
 */
@SuppressWarnings("ALL")
public class PaddingTest {

    boolean a;
    byte b;

    short c;
    char d;

    int e;
    float f;

    long g;
    double h;

    private static Unsafe UNSAFE;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println("field a --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("a")));
        System.out.println("field b --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("b")));
        System.out.println("field c --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("c")));
        System.out.println("field d --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("d")));
        System.out.println("field e --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("e")));
        System.out.println("field f --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("f")));
        System.out.println("field g --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("g")));
        System.out.println("field h --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("h")));

        PaddingTest paddingTest = new PaddingTest();

        System.out.println("Shallow Size: "+ MemoryUtil.memoryUsageOf(paddingTest));
        System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(paddingTest));
    }

}

結果如下:

field a --> 40
field b --> 41
field c --> 36
field d --> 38
field e --> 12
field f --> 32
field g --> 16
field h --> 24
Shallow Size: 48
Retained Size: 48

可以看到,先分配的是int類型的變量e,因為它正好是4個字節,其余的都是先從g和h變量開始分配的,因為這兩個變量是long類型和double類型的,占64位,最后分配的是a和b,它們只占一個字節。

如果分配到最后,這時字節數不是8的倍數,則需要padding。這里實際的大小是42字節,所以padding6字節,最終占用48字節。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,045評論 6 13
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,709評論 18 399
  • Redis的內存優化 聲明:本文內容來自《Redis開發與運維》一書第八章,如轉載請聲明。 Redis所有的數據都...
    meng_philip123閱讀 18,910評論 2 29
  • 今天以另一種方式上課,每個孩子需要得滿5個勾就進入純玩的狀態!結果發現孩子們都仿佛像打了雞血一樣認真得...,為了...
    明期特RB當家花旦閱讀 183評論 0 0
  • 以前上高中沒覺得滿課有什么不舒服,反而覺得很充實討厭的星期五它天經地義。可是自從步入大學,感覺好多課特別是周二周五...
    丫丫孩紙啊閱讀 406評論 2 2