解析一個Java對象佔用多少內存空間

說明: alignment, 對齊, 比如8字節的數據類型long, 在內存中的起始地址必須是8的整數倍。

padding, 補齊; 在對象所佔據空間的末尾,如果有空白, 需要使用padding來補齊, 因爲下一個對象的起始位置必須是4/8字節(32bit/64bit)的整數倍(這又是一種對齊)。

問題描述

一個對象具有100個屬性, 與100個對象每個具有1個屬性, 哪個佔用的內存空間更大?

一個對象會分配多少內存?

每增加一個屬性,對象佔用的空間會增加多少?

答案1

參考 Mindprod , 可以發現事情並不簡單:

JVM具體實現可以用任意形式來存儲內部數據, 可以是大端字節序或者小端字節序(big or little endian), 還可以增加任意數量的補齊、或者開銷, 儘管原生數據類型(primitives)的行爲必須符合規範。
例如, JVM或者本地編譯器可以決定是否將 boolean[] 存儲爲64bit的內存塊中, 類似於 BitSet。 廠商可以不告訴你這些細節, 只要程序運行結果一致即可。

  • JVM可以在棧(stack)空間分配一些臨時對象。
  • 編譯器可能用常量來替換某些變量或方法調用。
  • JVM可能對方法和循環生成多個編譯版本; 例如, 編譯兩種版本的方法, 針對某些情況調用其中的一個。

當然, 硬件平臺和操作系統還會有多級緩存, 例如CPU內置的L1/L2/L3; SRAM緩存, DRAM緩存, 普通內存, 以及磁盤上的虛擬內存。 用戶數據可能在多個層級的緩存中出現. 這麼多複雜的情況、決定了我們只能對內存佔用情況進行大致的估測。

測量方法

可以使用 Instrumentation.getObjectSize() 方法來估算一個對象佔用的內存空間。

想要查看對象的實際佈局(layout)、佔用(footprint)、以及引用(reference), 可以使用OpenJDK提供的 JOL工具(Java Object Layout)

對象頭和對象引用

在64位JVM中, 對象頭佔據的空間是 12-byte(=96bit=64+32), 但是以8字節對齊, 所以一個空類的實例至少佔用16字節。

在32位JVM中, 對象頭佔8個字節, 以4的倍數對齊(32=4*8)。(請參考 Dmitry Spikhalskiy,Jayen的回答,以及JavaWorld網站)。

通常, 在32位JVM, 以及內存小於 -Xmx32G 的64位JVM上, 一個引用佔的內存是4個字節。(指針壓縮)

因此, 64位JVM一般需要多消耗 30%-50% 堆內存。(參考: Should I use a 32- or a 64-bit JVM?, 2012, JDK 1.7)

包裝類型、數組和字符串

包裝類型比原生數據類型消耗的內存要多, 參考 JavaWorld :

  • Integer: 佔用16字節(8+4=12+補齊), 因爲 int 部分佔4個字節。 所以使用 Integer 比原生類型 int 要多消耗 300% 的內存。
  • Long: 一般佔用16個字節(8+8=16): 當然, 對象的實際大小由底層平臺的內存對齊確定, 具體由特定CPU平臺的JVM實現決定。 看起來一個Long 類型的對象, 比起原生類型long多佔用了8個字節。 相比之下, Integer有4字節的補齊, 很可能是因爲JVM強制進行了8字節的邊界對齊。

其他容器佔用的空間也不小:

  • 多維數組: 這是另一個驚喜。
    在進行數值或科學計算時, 開發人員經常會使用 int[dim1][dim2] 這種構造方式。
    在二維數組 int[dim1][dim2] 中, 每個嵌套的數組 int[dim2] 都是一個單獨的 Object, 會額外佔用16字節的空間。某些情況下,這種開銷是一種浪費。當數組維度更大時,這種開銷特別明顯。
    例如, int[128][2] 實例佔用3600字節。 而 int[256] 實例則只佔用1040字節。裏面的有效存儲空間是一樣的, 3600比起1040多了246%的額外開銷。在極端情況下, byte[256][1], 額外開銷的比例是19倍! 而在 C/C++ 中, 同樣的語法卻不增加額外的存儲開銷。

  • String: String 對象的空間隨着內部字符數組的增長而增長。當然, String 類的對象有24個字節的額外開銷。

    對於10字符以內的非空 String, 增加的開銷比起有效載荷(每個字符2字節 + 4個字節的length), 多佔用了100%到400%的內存。

對齊(Alignment)

看下面的 示例對象:

class X {                      // 8 字節-指向class定義的引用
   int a;                      // 4 字節
   byte b;                     // 1 字節
   Integer c = new Integer();  // 4 字節的引用
}

新手可能會認爲, 一個X類的實例佔用17字節的空間。 但由於需要對齊,也可稱爲補齊(padding), JVM分配的內存是8字節的整數倍, 所以佔用的空間不是17字節,而是24字節。

當然,運行JOL的示例之後,會發現JVM會依次先排列 parent-class 的fields, 然後到本class的字段時,也是先排列8字節的,排完了8字節的再排4字節的field,以此類推。當然,還會加塞子_ 儘量不浪費空間。

Java內置的序列化,也會基於這個佈局,帶來的坑就是加字段後就不兼容了。 只加方法不固定 serialVersionUID 也出問題。 所以有點經驗的都不喜歡用內置序列化,例如自定義類型存到redis時。

JOL使用示例

JOL (Java Object Layout) 是分析JVM中內存佈局的小工具, 通過 Unsafe, JVMTI, 以及 Serviceability Agent (SA) 來解碼實際的對象佈局,佔用,引用。 所以 JOL 比起基於 heap dump, 或者基於規範的其他工具來得準確。

JOL的官網地址爲: http://openjdk.java.net/projects/code-tools/jol/

從中可以看到:

參考:

本文最新翻譯地址: https://github.com/cncounter/translation/blob/master/tiemao_2018/37_java_object-memory_consumption/37_java_object-memory_consumption.md

翻譯日期: 2019年7月4日

翻譯人員: 鐵錨 https://renfufei.blog.csdn.net/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章