Java 對象大小的精確計算方法
日常使用Java進行業(yè)務(wù)開發(fā)時,我們基本不關(guān)心一個Java對象的大小,所以經(jīng)常因為錯誤的估算導(dǎo)致大量的內(nèi)存空間在無形之間被浪費了,所以今天筆者就基于這篇文章來聊聊一個Java對象的大小,希望對讀者日常堆內(nèi)存評估有所幫助。
一、Java對象構(gòu)成詳解
1.整體構(gòu)成概述
我們這里就以Hotspot虛擬機來探討Java對象的構(gòu)成,如下所示,可以看到Java對象的整體構(gòu)成分為:
- 對象頭(Header)
- 實例數(shù)據(jù)(Instance Data)
- 對齊填充(Padding)
2.對象頭
(1) Mark World
而對象頭是由兩部分組成的,第一部分用于存儲對象自身的數(shù)據(jù),也就是我們常說的Mark World,它記錄著一個對象的如下信息:
- 哈希碼(hashCode)
- GC分代年齡
- 鎖狀態(tài)標志
- 線程持有鎖
- 偏向鎖id
- 偏向時間戳
(2) 類型指針
再來說說類型指針,它記錄著當前對象的元數(shù)據(jù)的地址,虛擬機可通過這個指針確定當前對象屬于哪個類的實例,也就是說如果我們希望獲得這個對象的元數(shù)據(jù)信息是可以通過類型指針定位到。 需要注意的是,在JDK8版本默認情況下,Mark World默認開啟了指針壓縮,這使得這一部分在64位的操作系統(tǒng)中的情況下,長度由原來的8個字節(jié)(64位)變?yōu)?個字節(jié)(32位)。
(3) 數(shù)組長度
最后一部分就是數(shù)組長度,如果當前對象是基本類型的數(shù)組,那么這4位則是記錄數(shù)組的長度,為什么說是基本類型呢?原因很簡單,普通Java對象的的大小是可以通過元數(shù)據(jù)信息計算獲得,而基本類型的數(shù)組卻卻無法從元數(shù)據(jù)信息中計算獲得,所以我們就需要通過4個字節(jié)記錄一下數(shù)組的長度以便計算。
3.實例數(shù)據(jù)
這一點就不多說了,這就是對象真正存儲的有效信息,這些實例數(shù)據(jù)可以是從父類繼承也可以是自定義字段,因為實例數(shù)據(jù)可能存在多個,Hotspot虛擬機定義了實例對象內(nèi)存分配的先后順序:
- long/double(8字節(jié))
- int(4字節(jié))
- short/char(2字節(jié))
- byte/boolean(1字節(jié))
- oops(Ordinary Object Pointers 普通對象指針)
4.對齊填充
Hotspot虛擬機為了保證在指針壓縮的情況下,32字節(jié)的空間仍然表示32G的內(nèi)存空間地址,用到了8位對齊填充的思想,既保證了緩存命中率可以記錄更多的對象,又能記錄更多的對象地址。 因為指針壓縮涉及的知識點比較多,筆者后續(xù)會單獨開一個篇幅進行補充,這里我們有先說一下對其填充,假設(shè)我們現(xiàn)在有這樣一個Java對象,可以看到在實例數(shù)據(jù)部分,它有8字節(jié)的long變量和4字節(jié)的int變量,合起來是12字節(jié):
public
class
Obj
{
private
long id;
private
int age;
}
而8位對齊填充的意思就是實例數(shù)據(jù)部分的和要能夠被16整除,所以對于這個對象的實例部分,我們還需要補充4個字節(jié)做到8位的對齊填充:
二、基于JOL了解Java對象的構(gòu)成
1.前置步驟
了解了Java對象的組成之后,我們不妨通過JOL(Java Object Layout)來印證一下筆者的觀點,所以我們需要在項目中引入下面這個依賴開始本次的實驗:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
2.空對象
首先是一個空對象EmptyObj ,可以看到這個對象沒有任何成員變量:
class
EmptyObj
{
}
我們都知道默認情況下,JDK8是開啟指針壓縮的,可以看到object header總共12字節(jié),其中Mark World占了前8字節(jié)(4+4),類型指針占了4字節(jié),加起來是12字節(jié),而Java對象要求16位對齊,所以需要補齊4位,總的結(jié)果是16字節(jié):
com.sharkChili.webTemplate.EmptyObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我們再來看看關(guān)閉指針的壓縮的結(jié)果,首先我們設(shè)置JVM參數(shù)將指針壓縮關(guān)閉:
-XX:-UseCompressedClassPointers
此時我們就發(fā)現(xiàn)指針由原來是object header多了4位,原本被壓縮的指針占用空間被還原了(offset為8-12的部分),總的計算結(jié)果為16字節(jié),無需對齊填充:
com.sharkChili.webTemplate.EmptyObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) c0 34 b8 1c (11000000 00110100 10111000 00011100) (481834176)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
3.數(shù)組對象
我們再來看看數(shù)組對象,在默認開啟指針壓縮的情況下,我們創(chuàng)建了一個長度為3的數(shù)組:
com.sharkChili.webTemplate.EmptyObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) c0 34 b8 1c (11000000 00110100 10111000 00011100) (481834176)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到:
- Mark World占了8字節(jié)
- 指針4字節(jié)(offfset為8這一部分)
- offset為12這一部分也有了4字節(jié)的空間,記錄了一個值3即數(shù)組長度
所以8+4+4=16,對象頭剛剛好8位對齊,故無需對齊填充。
再看看實例數(shù)據(jù)部分(offset為16)這一部分,因為數(shù)組中有3個整形所以長度size為12,需要補充4字節(jié)達到8位對齊,最終這個數(shù)組對象的長度為16(對象頭)+16(實例數(shù)據(jù)部分)=32字節(jié):
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
16 12 int [I.<elements> N/A
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我們再來看看關(guān)閉指針壓縮的結(jié)果,可以看到mark word和指針都占了8位,加上數(shù)組長度的4位,最終對象頭為20位,8位對齊后為24位。 同理實例部分還是12字節(jié)的數(shù)組元素大小加4字節(jié)的8對齊字節(jié),關(guān)閉指針壓縮后的對象大小為40字節(jié):
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 68 0b 85 1c (01101000 00001011 10000101 00011100) (478481256)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
20 4 (alignment/padding gap)
24 12 int [I.<elements> N/A
36 4 (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 4 bytes internal + 4 bytes external = 8 bytes total
4.帶有成員變量的對象
我們再來說說帶有成員變量的Java對象,也就是我們?nèi)粘J褂玫钠胀↗ava對象:
class
NormalObject
{
int a;
short b;
byte c;
}
默認開啟指針壓縮的情況下,對象頭為8+4=12字節(jié),而實例數(shù)據(jù)部分,參考上文的實例數(shù)據(jù)順序,我們的NormalObject的實例數(shù)據(jù)內(nèi)存分配順序為int、short、byte。 虛擬機為了更好的利用內(nèi)存空間,看到對象頭還差4字節(jié)才能保證對象頭8位對齊填充,故將實例數(shù)據(jù)int作為對齊填充移動至對象頭。
所以實例數(shù)據(jù)部分長度是2+1+5(對齊填充),最終在指針壓縮的情況下,當前對象長度為24字節(jié)。
com.sharkChili.webTemplate.NormalObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int NormalObject.a 0
16 2 short NormalObject.b 0
18 1 byte NormalObject.c 0
19 5 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 5 bytes external = 5 bytes total
同理,關(guān)閉指針壓縮,相比讀者現(xiàn)在也知道如何計算了,筆者這里就不多贅述了,答案是是對象頭8+8,實例數(shù)據(jù)4+2+1+1(對齊填充),即關(guān)閉指針壓縮情況下,當前普通對象大小為24字節(jié):
com.sharkChili.webTemplate.NormalObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 10 35 0b 1d (00010000 00110101 00001011 00011101) (487273744)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 int NormalObject.a 0
20 2 short NormalObject.b 0
22 1 byte NormalObject.c 0
23 1 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total
5.帶有數(shù)組的對象
最后我們再來看看帶有數(shù)組的對象:
class NormalObject {
int a;
short b;
byte c;
int[] arr = new int[3];
}
先來看看開啟指針壓縮8+4+int變量作為對齊填充即16字節(jié),注意很多讀者會認為此時還需要計算數(shù)組長度,實際上數(shù)組長度記錄的是當前對象為數(shù)組情況下的數(shù)組的長度,而非成員變量的數(shù)組長度,所以我們的對象頭總的大小就是16。
然后實例數(shù)據(jù)部分4+2+1+1(對齊填充),最后就是數(shù)組引用4+4(對齊填充),最終結(jié)果為16+8+8即32:
com.sharkChili.webTemplate.NormalObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int NormalObject.a 0
16 2 short NormalObject.b 0
18 1 byte NormalObject.c 0
19 1 (alignment/padding gap)
20 4 int[] NormalObject.arr [0, 0, 0]
Instance size: 24 bytes
Space losses: 1 bytes internal + 0 bytes external = 1 bytes total
關(guān)閉指針壓縮情況下,對象頭8+8。實例數(shù)據(jù)4+2+1+1(對齊填充),再加上數(shù)組引用的4字節(jié)+4字對齊填充,最終計算結(jié)果為32字節(jié)。
com.sharkChili.webTemplate.NormalObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 48 35 f8 1c (01001000 00110101 11111000 00011100) (486028616)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 int NormalObject.a 0
20 2 short NormalObject.b 0
22 1 byte NormalObject.c 0
23 1 (alignment/padding gap)
24 4 int[] NormalObject.arr [0, 0, 0]
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 1 bytes internal + 4 bytes external = 5 bytes total
小結(jié)
總的來說要想獲取Java對象的大小,我們只需按照如下步驟即可精確計算:
- mark world 8位。
- 確認是否開啟指針壓縮,以計算類型指針大小。
- 是否是數(shù)組,若是則增加4字節(jié)數(shù)組長度位。
- 計算對象頭總和進行8位填充。
- 實例數(shù)據(jù)按照順序排列并計算總和,并進行8位填充。
- 引用數(shù)據(jù)計算總和,并進行8位填充。
- 綜合上述計算結(jié)果。