博客鏈接:http://www.ideabuffer.cn/2017/05/06/Java對(duì)象內(nèi)存布局/
我們知道在Java中基本數(shù)據(jù)類型的大小,例如int類型占4個(gè)字節(jié)、long類型占8個(gè)字節(jié),那么Integer對(duì)象和Long對(duì)象會(huì)占用多少內(nèi)存呢?本文介紹一下Java對(duì)象在堆中的內(nèi)存結(jié)構(gòu)以及對(duì)象大小的計(jì)算。
對(duì)象的內(nèi)存布局
一個(gè)Java對(duì)象在內(nèi)存中包括對(duì)象頭、實(shí)例數(shù)據(jù)和補(bǔ)齊填充3個(gè)部分:

對(duì)象頭
- Mark Word:包含一系列的標(biāo)記位,比如輕量級(jí)鎖的標(biāo)記位,偏向鎖標(biāo)記位等等。在32位系統(tǒng)占4字節(jié),在64位系統(tǒng)中占8字節(jié);
- Class Pointer:用來(lái)指向?qū)ο髮?duì)應(yīng)的Class對(duì)象(其對(duì)應(yīng)的元數(shù)據(jù)對(duì)象)的內(nèi)存地址。在32位系統(tǒng)占4字節(jié),在64位系統(tǒng)中占8字節(jié);
- Length:如果是數(shù)組對(duì)象,還有一個(gè)保存數(shù)組長(zhǎng)度的空間,占4個(gè)字節(jié);
對(duì)象實(shí)際數(shù)據(jù)
對(duì)象實(shí)際數(shù)據(jù)包括了對(duì)象的所有成員變量,其大小由各個(gè)成員變量的大小決定,比如:byte和boolean是1個(gè)字節(jié),short和char是2個(gè)字節(jié),int和float是4個(gè)字節(jié),long和double是8個(gè)字節(jié),reference是4個(gè)字節(jié)(64位系統(tǒng)中是8個(gè)字節(jié))。
| Primitive Type | Memory Required(bytes) |
|---|---|
| boolean | 1 |
| byte | 1 |
| short | 2 |
| char | 2 |
| int | 4 |
| float | 4 |
| long | 8 |
| double | 8 |
對(duì)于reference類型來(lái)說,在32位系統(tǒng)上占用4bytes, 在64位系統(tǒng)上占用8bytes。
對(duì)齊填充
Java對(duì)象占用空間是8字節(jié)對(duì)齊的,即所有Java對(duì)象占用bytes數(shù)必須是8的倍數(shù)。例如,一個(gè)包含兩個(gè)屬性的對(duì)象:int和byte,這個(gè)對(duì)象需要占用8+4+1=13個(gè)字節(jié),這時(shí)就需要加上大小為3字節(jié)的padding進(jìn)行8字節(jié)對(duì)齊,最終占用大小為16個(gè)字節(jié)。
注意:以上對(duì)64位操作系統(tǒng)的描述是未開啟指針壓縮的情況,關(guān)于指針壓縮會(huì)在下文中介紹。
對(duì)象頭占用空間大小
這里說明一下32位系統(tǒng)和64位系統(tǒng)中對(duì)象所占用內(nèi)存空間的大?。?/p>
- 在32位系統(tǒng)下,存放Class Pointer的空間大小是4字節(jié),MarkWord是4字節(jié),對(duì)象頭為8字節(jié);
- 在64位系統(tǒng)下,存放Class Pointer的空間大小是8字節(jié),MarkWord是8字節(jié),對(duì)象頭為16字節(jié);
- 64位開啟指針壓縮的情況下,存放Class Pointer的空間大小是4字節(jié),
MarkWord是8字節(jié),對(duì)象頭為12字節(jié); - 如果是數(shù)組對(duì)象,對(duì)象頭的大小為:數(shù)組對(duì)象頭8字節(jié)+數(shù)組長(zhǎng)度4字節(jié)+對(duì)齊4字節(jié)=16字節(jié)。其中對(duì)象引用占4字節(jié)(未開啟指針壓縮的64位為8字節(jié)),數(shù)組
MarkWord為4字節(jié)(64位未開啟指針壓縮的為8字節(jié)); - 靜態(tài)屬性不算在對(duì)象大小內(nèi)。
指針壓縮
從上文的分析中可以看到,64位JVM消耗的內(nèi)存會(huì)比32位的要多大約1.5倍,這是因?yàn)閷?duì)象指針在64位JVM下有更寬的尋址。對(duì)于那些將要從32位平臺(tái)移植到64位的應(yīng)用來(lái)說,平白無(wú)辜多了1/2的內(nèi)存占用,這是開發(fā)者不愿意看到的。
從JDK 1.6 update14開始,64位的JVM正式支持了 -XX:+UseCompressedOops 這個(gè)可以壓縮指針,起到節(jié)約內(nèi)存占用的新參數(shù)。
什么是OOP?
OOP的全稱為:Ordinary Object Pointer,就是普通對(duì)象指針。啟用CompressOops后,會(huì)壓縮的對(duì)象:
- 每個(gè)Class的屬性指針(靜態(tài)成員變量);
- 每個(gè)對(duì)象的屬性指針;
- 普通對(duì)象數(shù)組的每個(gè)元素指針。
當(dāng)然,壓縮也不是所有的指針都會(huì)壓縮,對(duì)一些特殊類型的指針,JVM是不會(huì)優(yōu)化的,例如指向PermGen的Class對(duì)象指針、本地變量、堆棧元素、入?yún)?、返回值和NULL指針不會(huì)被壓縮。
啟用指針壓縮
在Java程序啟動(dòng)時(shí)增加JVM參數(shù):-XX:+UseCompressedOops來(lái)啟用。
注意:32位HotSpot VM是不支持UseCompressedOops參數(shù)的,只有64位HotSpot VM才支持。
本文中使用的是JDK 1.8,默認(rèn)該參數(shù)就是開啟的。
查看對(duì)象的大小
接下來(lái)我們使用http://www.javamex.com/中提供的classmexer.jar來(lái)計(jì)算對(duì)象的大小。
運(yùn)行環(huán)境:JDK 1.8,Java HotSpot(TM) 64-Bit Server VM
基本數(shù)據(jù)類型
對(duì)于基本數(shù)據(jù)類型來(lái)說,是比較簡(jiǎn)單的,因?yàn)槲覀円呀?jīng)知道每個(gè)基本數(shù)據(jù)類型的大小。代碼如下:
/**
* VM options:
* -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
* -XX:+UseCompressedOops
*/
public class TestObjectSize {
int a;
long b;
static int c;
public static void main(String[] args) throws IOException {
TestObjectSize testObjectSize = new TestObjectSize();
// 打印對(duì)象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
// 打印對(duì)象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
System.in.read();
}
}
注意:在運(yùn)行前需要設(shè)置javaagent參數(shù),在JVM啟動(dòng)參數(shù)中添加-javaagent:/path_to_agent/classmexer.jar來(lái)運(yùn)行。
有關(guān)Shallow Size和Retained Size請(qǐng)參考http://blog.csdn.net/e5945/article/details/7708253。
開啟指針壓縮的情況
運(yùn)行查看結(jié)果:
Shallow Size: 24 bytes
Retained Size: 24 bytes
根據(jù)上文的分析可以知道,64位開啟指針壓縮的情況下:
- 對(duì)象頭大小=Class Pointer的空間大小為4字節(jié)+
MarkWord為8字節(jié)=12字節(jié); - 實(shí)際數(shù)據(jù)大小=int類型4字節(jié)+long類型8字節(jié)=12字節(jié)(靜態(tài)變量不在計(jì)算范圍之內(nèi))
在MAT中分析的結(jié)果如下:

所以大小是24字節(jié)。其實(shí)這里并沒有padding,因?yàn)檎檬?4字節(jié)。如果我們把long b;換成int b;之后,再來(lái)看一下結(jié)果:
Shallow Size: 24 bytes
Retained Size: 24 bytes
大小并沒有變化,說明這里做了padding,并且padding的大小是4字節(jié)。
這里的Shallow Size和Retained Size是一樣的,因?yàn)槎际腔緮?shù)據(jù)類型。
關(guān)閉指針壓縮的情況
如果要關(guān)閉指針壓縮,在JVM參數(shù)中添加-XX:-UseCompressedOops來(lái)關(guān)閉,再運(yùn)行上述代碼查看結(jié)果:
Shallow Size: 24 bytes
Retained Size: 24 bytes
分析一下在64位未開啟指針壓縮的情況下:
- 對(duì)象頭大小=Class Pointer的空間大小為8字節(jié)+
MarkWord為8字節(jié)=16字節(jié); - 實(shí)際數(shù)據(jù)大小=int類型4字節(jié)+long類型8字節(jié)=12字節(jié)(靜態(tài)變量不在計(jì)算范圍之內(nèi));
這里計(jì)算后大小為16+12=28字節(jié),這時(shí)候就需要padding來(lái)補(bǔ)齊了,所以padding為4字節(jié),最后的大小就是32字節(jié)。
我們?cè)侔?code>long b;換成int b;之后呢?通過上面的計(jì)算結(jié)果可以知道,實(shí)際數(shù)據(jù)大小就應(yīng)該是int類型4字節(jié)+int類型4字節(jié)=8字節(jié),對(duì)象頭大小為16字節(jié),那么不需要做padding,對(duì)象的大小為24字節(jié):
Shallow Size: 24 bytes
Retained Size: 24 bytes
數(shù)組類型
64位系統(tǒng)中,數(shù)組對(duì)象的對(duì)象頭占用24 bytes,啟用壓縮后占用16字節(jié)。比普通對(duì)象占用內(nèi)存多是因?yàn)樾枰~外的空間存儲(chǔ)數(shù)組的長(zhǎng)度?;A(chǔ)數(shù)據(jù)類型數(shù)組占用的空間包括數(shù)組對(duì)象頭以及基礎(chǔ)數(shù)據(jù)類型數(shù)據(jù)占用的內(nèi)存空間。由于對(duì)象數(shù)組中存放的是對(duì)象的引用,所以數(shù)組對(duì)象的Shallow Size=數(shù)組對(duì)象頭+length * 引用指針大小,Retained Size=Shallow Size+length*每個(gè)元素的Retained Size。
代碼如下:
/**
* VM options:
* -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
* -XX:+UseCompressedOops
*/
public class TestObjectSize {
long[] arr = new long[6];
public static void main(String[] args) throws IOException {
TestObjectSize testObjectSize = new TestObjectSize();
// 打印對(duì)象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
// 打印對(duì)象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
System.in.read();
}
}
開啟指針壓縮的情況
結(jié)果如下:
Shallow Size: 16 bytes
Retained Size: 80 bytes
Shallow Size比較簡(jiǎn)單,這里對(duì)象頭大小為12字節(jié), 實(shí)際數(shù)據(jù)大小為4字節(jié),所以Shallow Size為16。
對(duì)于Retained Size來(lái)說,要計(jì)算數(shù)組占用的大小,對(duì)于數(shù)組來(lái)說,它的對(duì)象頭部多了一個(gè)用來(lái)存儲(chǔ)數(shù)組長(zhǎng)度的空間,該空間大小為4字節(jié),所以數(shù)組對(duì)象的大小=引用對(duì)象頭大小12字節(jié)+存儲(chǔ)數(shù)組長(zhǎng)度的空間大小4字節(jié)+數(shù)組的長(zhǎng)度數(shù)組中對(duì)象的Retained Size+padding大小*
下面分析一下上述代碼中的long[] arr = new long[6];,它是一個(gè)長(zhǎng)度為6的long類型的數(shù)組,由于long類型的大小為8字節(jié),所以數(shù)組中的實(shí)際數(shù)據(jù)是68=48字節(jié),那么數(shù)組對(duì)象的大小=12+4+68+0=64,最終的Retained Size=Shallow Size + 數(shù)組對(duì)象大小=16+64=80。
通過MAT查看如下:

關(guān)閉指針壓縮的情況
結(jié)果如下:
Shallow Size: 24 bytes
Retained Size: 96 bytes
這個(gè)結(jié)果大家應(yīng)該能自己分析出來(lái)了,因?yàn)檫@時(shí)引用對(duì)象頭為16字節(jié),那么數(shù)組的大小=16+4+6*8+4=72,(這里最后一個(gè)4是padding),所以Retained Size=Shallow Size + 數(shù)組對(duì)象大小=24+72=96。
通過MAT查看如下:

包裝類型
包裝類(Boolean/Byte/Short/Character/Integer/Long/Double/Float)占用內(nèi)存的大小等于對(duì)象頭大小加上底層基礎(chǔ)數(shù)據(jù)類型的大小。
包裝類型的Retained Size占用情況如下:
| Numberic Wrappers | +useCompressedOops | -useCompressedOops |
|---|---|---|
| Byte, Boolean | 16 bytes | 24 bytes |
| Short, Character | 16 bytes | 24 bytes |
| Integer, Float | 16 bytes | 24 bytes |
| Long, Double | 24 bytes | 24 bytes |
代碼如下:
/**
* VM options:
* -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
* -XX:+UseCompressedOops
*/
public class TestObjectSize {
Boolean a = new Boolean(false);
Byte b = new Byte("1");
Short c = new Short("1");
Character d = new Character('a');
Integer e = new Integer(1);
Float f = new Float(2.5);
Long g = new Long(123L);
Double h = new Double(2.5D);
public static void main(String[] args) throws IOException {
TestObjectSize testObjectSize = new TestObjectSize();
// 打印對(duì)象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
// 打印對(duì)象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
System.in.read();
}
}
開啟指針壓縮的情況
結(jié)果如下:
Shallow Size: 48 bytes
Retained Size: 192 bytes
MAT中的結(jié)果如下:

關(guān)閉指針壓縮的情況
結(jié)果如下:
Shallow Size: 80 bytes
Retained Size: 272 bytes
MAT中的結(jié)果如下:

String類型
在JDK1.7及以上版本中,java.lang.String中包含2個(gè)屬性,一個(gè)用于存放字符串?dāng)?shù)據(jù)的char[], 一個(gè)int類型的hashcode, 部分源代碼如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
...
}
因此,在關(guān)閉指針壓縮時(shí),一個(gè)String對(duì)象的大小為:
Shallow Size=對(duì)象頭大小16字節(jié)+int類型大小4字節(jié)+數(shù)組引用大小8字節(jié)+padding4字節(jié)=32字節(jié);
Retained Size=Shallow Size+char數(shù)組的Retained Size。
在開啟指針壓縮時(shí),一個(gè)String對(duì)象的大小為:
Shallow Size=對(duì)象頭大小12字節(jié)+int類型大小4字節(jié)+數(shù)組引用大小4字節(jié)+padding4字節(jié)=24字節(jié);
Retained Size=Shallow Size+char數(shù)組的Retained Size。
代碼如下:
/**
* VM options:
* -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
* -XX:+UseCompressedOops
*/
public class TestObjectSize {
String s = "test";
public static void main(String[] args) throws IOException {
TestObjectSize testObjectSize = new TestObjectSize();
// 打印對(duì)象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
// 打印對(duì)象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
System.in.read();
}
}
開啟指針壓縮的情況
結(jié)果如下:
Shallow Size: 16 bytes
Retained Size: 64 bytes
MAT中的結(jié)果如下:

關(guān)閉指針壓縮的情況
結(jié)果如下:
Shallow Size: 24 bytes
Retained Size: 88 bytes
MAT中的結(jié)果如下:

其他引用類型的大小
根據(jù)上面的分析,可以計(jì)算出一個(gè)對(duì)象在內(nèi)存中的占用空間大小情況,其他的引用類型可以參考分析計(jì)算過程來(lái)計(jì)算內(nèi)存的占用情況。
關(guān)于padding
思考這樣一個(gè)問題,是不是padding都加到對(duì)象的后面呢,如果對(duì)象頭占12個(gè)字節(jié),對(duì)象中只有1個(gè)long類型的變量,那么該long類型的變量的偏移起始地址是在12嗎?用下面一段代碼測(cè)試一下:
@SuppressWarnings("ALL")
public class PaddingTest {
long a;
private static Unsafe UNSAFE;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws NoSuchFieldException {
System.out.println(UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("a")));
}
}
這里使用Unsafe類來(lái)查看變量的偏移地址,運(yùn)行后結(jié)果如下:
16
如果是換成int類型的變量呢?結(jié)果是12。
現(xiàn)在一般的CPU一次直接操作的數(shù)據(jù)可以到64位,也就是8個(gè)字節(jié),那么字長(zhǎng)就是64,而long類型本身就是占64位,如果這時(shí)偏移地址是12,那么需要分兩次讀取該數(shù)據(jù),而如果偏移地址從16開始只需要通過一次讀取即可。int類型的數(shù)據(jù)占用4個(gè)字節(jié),所以可以從12開始。
把上面的代碼修改一下:
@SuppressWarnings("ALL")
public class PaddingTest {
long a;
byte b;
byte c;
private static Unsafe UNSAFE;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws NoSuchFieldException {
System.out.println(UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("a")));
System.out.println(UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("b")));
System.out.println(UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("c")));
}
}
運(yùn)行結(jié)果如下:
16
12
13
在本例中,如果變量的大小小于等于4個(gè)字節(jié),那么在分配內(nèi)存的時(shí)候會(huì)先優(yōu)先分配,因?yàn)檫@樣可以減少padding,比如這里的b和c變量;如果這時(shí)達(dá)到了16個(gè)字節(jié),那么其他的變量按照類型所占內(nèi)存的大小降序分配。
再次修改代碼:
/**
* VM options: -javaagent:D:\source-files\classmexer.jar
*/
@SuppressWarnings("ALL")
public class PaddingTest {
boolean a;
byte b;
short c;
char d;
int e;
float f;
long g;
double h;
private static Unsafe UNSAFE;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws NoSuchFieldException {
System.out.println("field a --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("a")));
System.out.println("field b --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("b")));
System.out.println("field c --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("c")));
System.out.println("field d --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("d")));
System.out.println("field e --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("e")));
System.out.println("field f --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("f")));
System.out.println("field g --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("g")));
System.out.println("field h --> "+ UNSAFE.objectFieldOffset(PaddingTest.class.getDeclaredField("h")));
PaddingTest paddingTest = new PaddingTest();
System.out.println("Shallow Size: "+ MemoryUtil.memoryUsageOf(paddingTest));
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(paddingTest));
}
}
結(jié)果如下:
field a --> 40
field b --> 41
field c --> 36
field d --> 38
field e --> 12
field f --> 32
field g --> 16
field h --> 24
Shallow Size: 48
Retained Size: 48
可以看到,先分配的是int類型的變量e,因?yàn)樗檬?個(gè)字節(jié),其余的都是先從g和h變量開始分配的,因?yàn)檫@兩個(gè)變量是long類型和double類型的,占64位,最后分配的是a和b,它們只占一個(gè)字節(jié)。
如果分配到最后,這時(shí)字節(jié)數(shù)不是8的倍數(shù),則需要padding。這里實(shí)際的大小是42字節(jié),所以padding6字節(jié),最終占用48字節(jié)。