Java对象内存结构
轉載自?Java對象內存結構
學C/C++出身的我,對Java有一點非常困惑,那就是缺乏計算對象占用內存大小的機制。而在C++中就可以通過sizeof運算符來獲得基本類型以及類實例的大小。C和C++中的這個操作符對于指針運算、內存拷貝和IO操作都非常有用。
Java中并沒有一個類似的運算符。事實上,Java也不需要這種運算符。Java中基本類型的大小在語言規范中已經定義了,而C/C++中基本類型大小則跟平臺相關。Java有自己的通過序列化構建的IO框架。再者,由于Java中沒有指針,因此指針運算和內存塊拷貝之類的操作也不存在。
但是,Java程序員有時還是希望能知道一個Java對象到底用了多少內存的。不過這個問題的答案并不簡單。
首先要區分清楚的是shallow size和deep size。Shallow size是指對象自身占用的內存大小,其引用對象的大小不算在內。而deep size,則是自身所占內存大小和其遞歸引用的所有對象所占內存大小的總和。大多數情況下,你會希望獲得一個對象的deep size,但是為了知道這個值,首先要知道怎么算shallow size,下面我來介紹一下。
有人抱怨JVM規范中沒有針對運行時Java對象的內存結構的說明,這也就是說JVM供應商可以按照自己的需要來實現這一點。后果就是,同一個類在不同的JVM上運行的實例對象占用的內存大小會有差別。好在是世界上大部分人(包括我在內)都使用Sun HotSpot虛擬機,這就大大簡化了這個問題。我們接下來的討論也會基于32位的Sun公司的JVM。下面我介紹一些規則來輔助解釋JVM如何組織對象在內存中的布局的。
沒有實例屬性的類的內存布局
在Sun JVM中,(除了數組之外的)對象都有兩個機器字(words)的頭部。第一個字中包含這個對象的標示哈希碼以及其他一些類似鎖狀態和等標識信息,第二個字中包含一個指向對象的類的引用。另外,任何對象都是8個字節為粒度進行對齊的。這就是對象內存布局的第一個規則:
規則1:任何對象都是8個字節為粒度進行對齊的。
比如,如果調用new Object(),由于Object類并沒有其他沒有其他可存儲的成員,那么僅僅使用堆中的8個字節來保存兩個字的頭部即可。
繼承了Object的類的內存布局
除了上面所說的8個字節的頭部,類屬性緊隨其后。屬性通常根據其大小來排列。例如,整型(int)以4個字節為單位對齊,長整型(long)以8個字節為單位對齊。這里是出于性能考慮而這么設計的:通常情況下,如果數據以4字節為單位對齊,那么從內存中讀4字節的數據并寫入到處理器的4字節寄存器是性價比更高的。
為了節省內存,Sun VM并沒有按照屬性聲明時的順序來進行內存布局。實際上,屬性在內存中按照下面的順序來組織:
1. 雙精度型(doubles)和長整型(longs)
2. 整型(ints)和浮點型(floats)
3. 短整型(shorts)和字符型(chars)
4. 布爾型(booleans)和字節型(bytes)
5. 引用類型(references)
內存使用率會通過這個機制得到優化。例如,如下聲明一個類:
| 12345678910111213 | classMyClass {???????bytea;???????intc;???????booleand;???????longe;???????Object f;????????? } |
如果JVM并沒有打亂屬性的聲明順序,其對象內存布局將會是下面這個樣子:
?
| 123456789 | [HEADER:?8bytes]? 8[a:??????1byte ]? 9[padding:3bytes] 12[c:??????4bytes] 16[d:??????1byte ] 17[padding:7bytes] 24[e:??????8bytes] 32[f:??????4bytes] 36[padding:4bytes] 40 |
此時,用于占位的14個字節是浪費的,這個對象一共使用了40個字節的內存空間。但是,如果用上面的規則對這些對象重新排序,其內存結果會變成下面這個樣子:
| 12345678 | [HEADER:?8bytes]? 8[e:??????8bytes] 16[c:??????4bytes] 20[a:??????1byte ] 21[d:??????1byte ] 22[padding:2bytes] 24[f:??????4bytes] 28[padding:4bytes] 32 |
這次,用于占位的只有6個字節,這個對象使用了32個字節的內存空間。
因此,對象內存布局的第二個規則是:
規則2:類屬性按照如下優先級進行排列:長整型和雙精度類型;整型和浮點型;字符和短整型;字節類型和布爾類型,最后是引用類型。這些屬性都按照各自的單位對齊。
現在我們知道如何計算一個繼承了Object的類的實例的內存大小了。下面這個例子用來做下練習: java.lang.Boolean。這是其內存布局:
| 123 | [HEADER:?8bytes]? 8[value:??1byte ]? 9[padding:7bytes] 16 |
Boolean類的實例占用16個字節的內存!驚訝吧?(別忘了最后用來占位的7個字節)。
繼承其他類的子類的內存布局
JVM所遵守的下面3個規則用來組織有父類的類的成員。對象內存布局的規則3如下:
規則3:不同類繼承關系中的成員不能混合排列。首先按照規則2處理父類中的成員,接著才是子類的成員。
舉例如下:
| 123456789 | classA {???longa;???intb;???intc;}classB extendsA {???longd;} |
?
類B的實例在內存中的存儲如下:
| 12345 | [HEADER:?8bytes]? 8[a:??????8bytes] 16[b:??????4bytes] 20[c:??????4bytes] 24[d:??????8bytes] 32 |
如果父類中的成員的大小無法滿足4個字節這個基本單位,那么下一條規則就會起作用:
規則4:當父類中最后一個成員和子類第一個成員的間隔如果不夠4個字節的話,就必須擴展到4個字節的基本單位。
舉例如下:
| 123456789101112 | classA {???bytea;}classB {???byteb;}[HEADER:?8bytes]? 8[a:??????1byte ]? 9[padding:3bytes] 12[b:??????1byte ] 13[padding:3bytes] 16 |
注意到成員a被擴充了3個字節以保證和成員b之間的間隔是4個字節。這個空間不能被類B使用,因此被浪費了。
最后一條規則在下面情況下用來節省一些空間:如果子類成員是長整型或雙精度類型,并且父類并沒有用完8個字節。
規則5:如果子類第一個成員是一個雙精度或者長整型,并且父類并沒有用完8個字節,JVM會破壞規則2,按照整形(int),短整型(short),字節型(byte),引用類型(reference)的順序,向未填滿的空間填充。
舉例如下:
| 123456789 | classA {??bytea;}classB {??longb;??shortc;? ??byted;} |
其內存布局如下:
| 1234567 | [HEADER:?8bytes]? 8[a:??????1byte ]? 9[padding:3bytes] 12[c:??????2bytes] 14[d:??????1byte ] 15[padding:1byte ] 16[b:??????8bytes] 24 |
在第12字節處,類A“結束”的地方,JVM沒有遵守規則2,而是在長整型之前插入一個短整型和一個字節型成員,這樣可以避免浪費3個字節。
數組的內存布局
數組有一個額外的頭部成員,用來存放“長度”變量。數組元素以及數組本身,跟其他常規對象同樣,都需要遵守8個字節的邊界規則。
下面是一個有3個元素的字節數組的內存布局:
| 12345 | [HEADER:?12bytes] 12[[0]:?????1byte ] 13[[1]:?????1byte ] 14[[2]:?????1byte ] 15[padding:?1byte ] 16 |
下面是一個有3個元素的長整型數字的內存布局:
| 12345 | [HEADER:?12bytes] 12[padding:?4bytes] 16[[0]:?????8bytes] 24[[1]:?????8bytes] 32[[2]:?????8bytes] 40 |
內部類的內存布局
非靜態內部類(Non-static inner classes)有一個額外的“隱藏”成員,這個成員是一個指向外部類的引用變量。這個成員是一個普通引用,因此遵守引用內存布局的規則。內部類因此有4個字節的額外開銷。
最后的一點想法
我們已經學習了在32位Sun JVM中如何計算Java對象的shallow size。知道內存是如何組織的有助于理解類實例占用的內存數。
英文原文:Code Instructions,翻譯:ImportNew?-?鄭雯
譯文鏈接:?http://www.importnew.com/1305.html
總結
以上是生活随笔為你收集整理的Java对象内存结构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 曝华为Mate60系列供应商涨价20%?
- 下一篇: 消息称 OpenAI 将推出大更新,助力