Java對象的內存布局:
對象頭(Header)
實例數據(Instance Data)
對齊填充(Padding)
在網上搜到了一篇博客講的非常好:http://yueyemaitian.iteye.com/blog/2033046
大家可以用這個代碼邊看邊驗證,注意的是,運行這個程序需要通過javaagent注入Instrumentation,具體可以看原博客。我今天主要是總結下手動計算Java對象占用字節數的基本規則,下面詳細分析一下。
【1】對象頭
計算公式:
Class 模板對象頭 = Mark World(1字寬)+Class Metadata Address(1字寬)
+Array length(1字寬);
字寬就是字長,32位機上一個字長32位(4字節),64位機上一個字長64位(8字節)。
普通對象頭(非數組)在32位系統上占用8bytes,64位系統上占用16bytes。數組對象頭則分別為12字節和24字節。
空對象是只有對象頭,沒有實例數據,也無需填充對齊。因為所有的Java非基本類型的對象都需要默認繼承Object對象
,因此不論什么樣的Java對象,其大小都必須是大於8byte(16byte)
。
在32位機上包裝類型的大小至少是12byte(聲明一個空Object至少需要的空間),而且12byte沒有包含任何有效信息,同時,因為Java對象大小是8的整數倍,因此一個基本類型包裝類的大小至少是16byte。
這個內存占用是很恐怖的,它是使用基本類型的N倍(N>2),有些類型的內存占用更是誇張(隨便想下就知道了)。因此,可能的話應盡量少使用包裝類。在JDK5.0以后,因為加入了自動類型裝換,因此,Java虛擬機會在存儲方面進行相應的優化。
【2】實例數據
原生類型(primitive type)的內存占用如下:
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(一個字長)
。
Integer a = new Integer();
【3】對齊填充
HotSpot的對齊方式為8字節對齊
:
(對象頭 + 實例數據 + padding) % 8等於0,且0 <= padding < 8
【4】指針壓縮
對象占用的內存大小收到VM參數UseCompressedOops
的影響。
① 對對象頭的影響
開啟壓縮(-XX:+UseCompressedOops
)對象頭大小為12bytes(64位機器,不開啟壓縮則為16字節,開啟壓縮少了4字節-半個字長
)。
static class A {
int a;//4字節
}
A對象占用內存情況:
-
關閉指針壓縮: 16+4=20不是8的倍數,所以+padding/4=24
這里也可以看到無實例數據和對齊補充的Object實例對象在64位機上內存大小–16字節sizeOf(new Object())=16
。 -
開啟指針壓縮: 12+4=16已經是8的倍數了,不需要再padding。
② 對reference類型的影響
64位機器上reference類型占用8個字節,開啟指針壓縮后占用4個字節,少半個字長
。
static class B2 {
int b2a;//基本類型4字節
Integer b2b;//引用類型在64bit機上為8字節
}
B2對象占用內存情況:
-
關閉指針壓縮:16+4+8=28不是8的倍數,所以+padding/4=32
-
開啟指針壓縮: 12+4+4=20不是8的倍數,所以+padding/4=24
【5】數組對象
64位機器上,數組對象的對象頭占用24個字節,啟用壓縮之后占用16個字節,少了一個字長
。之所以比普通對象占用內存多是因為需要額外的空間存儲數組的長度。
先考慮下new Integer[0]占用的內存大小,長度為0,即是對象頭的大小:
- 未開啟壓縮:24bytes
- 開啟壓縮后:16bytes
接着計算new Integer[1],new Integer[2],new Integer[3]和new Integer[4]就很容易了:
- 未開啟壓縮:
- 開啟壓縮:
拿new Integer[3]來具體解釋下:
未開啟壓縮:24(對象頭)+8*3=48,不需要padding;
開啟壓縮:16(對象頭)+3*4=28,+padding/4=32,其他依次類推。
【6】復合對象
計算復合對象占用內存的大小其實就是運用上面幾條規則,只是麻煩點。
① 對象本身的大小
直接計算當前對象占用空間大小,包括當前類及超類的基本類型實例字段大小、引用類型實例字段引用大小、實例基本類型數組總占用空間、實例引用類型數組引用本身占用空間大小。但是不包括超類繼承下來的和當前類聲明的實例引用字段的對象本身的大小、實例引用數組引用的對象本身的大小。
static class B {
int a;
int b;
}
static class C {
int ba;
B[] as = new B[3];
C() {
for (int i = 0; i < as.length; i++) {
as[i] = new B();
}
}
}
C對象占用內存大小:
- 未開啟壓縮:16(對象頭)+4(ba)+8(as引用的大小)+padding/4=32
- 開啟壓縮:12+4+4+padding/4=24
② 當前對象占用的空間總大小
遞歸計算當前對象占用空間總大小,包括當前類和超類的實例字段大小以及實例字段引用對象大小。
遞歸計算復合對象占用的內存的時候需要注意的是:對齊填充是以每個對象為單位進行的,看下面這個圖就很容易明白。
現在我們來手動計算下C對象占用的全部內存是多少,主要是三部分構成:C對象本身的大小+數組對象的大小+B對象的大小。
- 未開啟壓縮:
(16 + 4 + 8+4(padding)) + (24+ 8*3) +(16+8)*3 = 152bytes
- 開啟壓縮:
(12 + 4 + 4 +4(padding)) + (16 + 4*3 +4(數組對象padding)) + (12+8+4(B對象padding))*3= 128bytes
實例分析為了復現這個問題,准備了4個簡單類:
class AAAAA {}
class BBBBB { int a = 1; }
class CCCCC { long a = 1L; }
class DDDDD { String s = "hello"; }
結果
有了對象各部分的內存占用大小,可以很輕松的計算出ABCD各對象在64位系統,且開啟UseCompressedOops
參數時的大小。
- A對象只包含一個對象頭,大小占12字節,不是8的倍數,需要加上4字節進行填充,一共占16字節
- B對象包含一個對象頭和int類型,12+4=16,正好是8的倍數,不需要填充。
- C對象包含一個對象頭和long類型,12+8=20,不是8的倍數,使用4個字節進行填充,占24字節
- D對象包含一個對象頭和引用類型,12+4=16,正好是8的倍數,不需要填充。
參考博文:
細探究,Java對象創建的奧秘
http://www.cnblogs.com/magialmoon/p/3757767.html
鏈接:https://www.jianshu.com/p/194b745884a5