說明: 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/
從中可以看到:
-
JOL支持命令行方式的調用, 即 jol-cli。
下載頁面請參考 Maven中央倉庫: http://central.maven.org/maven2/org/openjdk/jol/jol-cli/; 可下載其中的
jol-cli-0.9-full.jar
文件。 -
JOL還支持代碼方式調用, 示例: http://hg.openjdk.java.net/code-tools/jol/file/tip/jol-samples/src/main/java/org/openjdk/jol/samples/;
相關的依賴可以在Maven中央倉庫找到:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
參考:
翻譯日期: 2019年7月4日
翻譯人員: 鐵錨 https://renfufei.blog.csdn.net/