博客鏈接:http://www.ideabuffer.cn/2017/05/06/Java對象內存布局/
我們知道在Java中基本數據類型的大小,例如int類型占4個字節、long類型占8個字節,那么Integer對象和Long對象會占用多少內存呢?本文介紹一下Java對象在堆中的內存結構以及對象大小的計算。
對象的內存布局
一個Java對象在內存中包括對象頭、實例數據和補齊填充3個部分:
對象頭
- Mark Word:包含一系列的標記位,比如輕量級鎖的標記位,偏向鎖標記位等等。在32位系統占4字節,在64位系統中占8字節;
- Class Pointer:用來指向對象對應的Class對象(其對應的元數據對象)的內存地址。在32位系統占4字節,在64位系統中占8字節;
- Length:如果是數組對象,還有一個保存數組長度的空間,占4個字節;
對象實際數據
對象實際數據包括了對象的所有成員變量,其大小由各個成員變量的大小決定,比如:byte和boolean是1個字節,short和char是2個字節,int和float是4個字節,long和double是8個字節,reference是4個字節(64位系統中是8個字節)。
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。
對齊填充
Java對象占用空間是8字節對齊的,即所有Java對象占用bytes數必須是8的倍數。例如,一個包含兩個屬性的對象:int和byte,這個對象需要占用8+4+1=13個字節,這時就需要加上大小為3字節的padding進行8字節對齊,最終占用大小為16個字節。
注意:以上對64位操作系統的描述是未開啟指針壓縮的情況,關于指針壓縮會在下文中介紹。
對象頭占用空間大小
這里說明一下32位系統和64位系統中對象所占用內存空間的大小:
- 在32位系統下,存放Class Pointer的空間大小是4字節,MarkWord是4字節,對象頭為8字節;
- 在64位系統下,存放Class Pointer的空間大小是8字節,MarkWord是8字節,對象頭為16字節;
- 64位開啟指針壓縮的情況下,存放Class Pointer的空間大小是4字節,
MarkWord
是8字節,對象頭為12字節; - 如果是數組對象,對象頭的大小為:數組對象頭8字節+數組長度4字節+對齊4字節=16字節。其中對象引用占4字節(未開啟指針壓縮的64位為8字節),數組
MarkWord
為4字節(64位未開啟指針壓縮的為8字節); - 靜態屬性不算在對象大小內。
指針壓縮
從上文的分析中可以看到,64位JVM消耗的內存會比32位的要多大約1.5倍,這是因為對象指針在64位JVM下有更寬的尋址。對于那些將要從32位平臺移植到64位的應用來說,平白無辜多了1/2的內存占用,這是開發者不愿意看到的。
從JDK 1.6 update14開始,64位的JVM正式支持了 -XX:+UseCompressedOops 這個可以壓縮指針,起到節約內存占用的新參數。
什么是OOP?
OOP的全稱為:Ordinary Object Pointer,就是普通對象指針。啟用CompressOops后,會壓縮的對象:
- 每個Class的屬性指針(靜態成員變量);
- 每個對象的屬性指針;
- 普通對象數組的每個元素指針。
當然,壓縮也不是所有的指針都會壓縮,對一些特殊類型的指針,JVM是不會優化的,例如指向PermGen的Class對象指針、本地變量、堆棧元素、入參、返回值和NULL指針不會被壓縮。
啟用指針壓縮
在Java程序啟動時增加JVM參數:-XX:+UseCompressedOops
來啟用。
注意:32位HotSpot VM是不支持UseCompressedOops參數的,只有64位HotSpot VM才支持。
本文中使用的是JDK 1.8,默認該參數就是開啟的。
查看對象的大小
接下來我們使用http://www.javamex.com/中提供的classmexer.jar來計算對象的大小。
運行環境:JDK 1.8,Java HotSpot(TM) 64-Bit Server VM
基本數據類型
對于基本數據類型來說,是比較簡單的,因為我們已經知道每個基本數據類型的大小。代碼如下:
/**
* 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();
// 打印對象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
// 打印對象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
System.in.read();
}
}
注意:在運行前需要設置javaagent參數,在JVM啟動參數中添加-javaagent:/path_to_agent/classmexer.jar
來運行。
有關Shallow Size和Retained Size請參考http://blog.csdn.net/e5945/article/details/7708253。
開啟指針壓縮的情況
運行查看結果:
Shallow Size: 24 bytes
Retained Size: 24 bytes
根據上文的分析可以知道,64位開啟指針壓縮的情況下:
- 對象頭大小=Class Pointer的空間大小為4字節+
MarkWord
為8字節=12字節; - 實際數據大小=int類型4字節+long類型8字節=12字節(靜態變量不在計算范圍之內)
在MAT中分析的結果如下:
所以大小是24字節。其實這里并沒有padding,因為正好是24字節。如果我們把long b;
換成int b;
之后,再來看一下結果:
Shallow Size: 24 bytes
Retained Size: 24 bytes
大小并沒有變化,說明這里做了padding,并且padding的大小是4字節。
這里的Shallow Size和Retained Size是一樣的,因為都是基本數據類型。
關閉指針壓縮的情況
如果要關閉指針壓縮,在JVM參數中添加-XX:-UseCompressedOops
來關閉,再運行上述代碼查看結果:
Shallow Size: 24 bytes
Retained Size: 24 bytes
分析一下在64位未開啟指針壓縮的情況下:
- 對象頭大小=Class Pointer的空間大小為8字節+
MarkWord
為8字節=16字節; - 實際數據大小=int類型4字節+long類型8字節=12字節(靜態變量不在計算范圍之內);
這里計算后大小為16+12=28字節,這時候就需要padding來補齊了,所以padding為4字節,最后的大小就是32字節。
我們再把long b;
換成int b;
之后呢?通過上面的計算結果可以知道,實際數據大小就應該是int類型4字節+int類型4字節=8字節,對象頭大小為16字節,那么不需要做padding,對象的大小為24字節:
Shallow Size: 24 bytes
Retained Size: 24 bytes
數組類型
64位系統中,數組對象的對象頭占用24 bytes,啟用壓縮后占用16字節。比普通對象占用內存多是因為需要額外的空間存儲數組的長度。基礎數據類型數組占用的空間包括數組對象頭以及基礎數據類型數據占用的內存空間。由于對象數組中存放的是對象的引用,所以數組對象的Shallow Size=數組對象頭+length * 引用指針大小,Retained Size=Shallow Size+length*每個元素的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();
// 打印對象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
// 打印對象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
System.in.read();
}
}
開啟指針壓縮的情況
結果如下:
Shallow Size: 16 bytes
Retained Size: 80 bytes
Shallow Size比較簡單,這里對象頭大小為12字節, 實際數據大小為4字節,所以Shallow Size為16。
對于Retained Size來說,要計算數組占用的大小,對于數組來說,它的對象頭部多了一個用來存儲數組長度的空間,該空間大小為4字節,所以數組對象的大小=引用對象頭大小12字節+存儲數組長度的空間大小4字節+數組的長度數組中對象的Retained Size+padding大小*
下面分析一下上述代碼中的long[] arr = new long[6];
,它是一個長度為6的long類型的數組,由于long類型的大小為8字節,所以數組中的實際數據是68=48字節,那么數組對象的大小=12+4+68+0=64,最終的Retained Size=Shallow Size + 數組對象大小=16+64=80。
通過MAT查看如下:
關閉指針壓縮的情況
結果如下:
Shallow Size: 24 bytes
Retained Size: 96 bytes
這個結果大家應該能自己分析出來了,因為這時引用對象頭為16字節,那么數組的大小=16+4+6*8+4=72,(這里最后一個4是padding),所以Retained Size=Shallow Size + 數組對象大小=24+72=96。
通過MAT查看如下:
包裝類型
包裝類(Boolean/Byte/Short/Character/Integer/Long/Double/Float)占用內存的大小等于對象頭大小加上底層基礎數據類型的大小。
包裝類型的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();
// 打印對象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
// 打印對象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
System.in.read();
}
}
開啟指針壓縮的情況
結果如下:
Shallow Size: 48 bytes
Retained Size: 192 bytes
MAT中的結果如下:
關閉指針壓縮的情況
結果如下:
Shallow Size: 80 bytes
Retained Size: 272 bytes
MAT中的結果如下:
String類型
在JDK1.7及以上版本中,java.lang.String
中包含2個屬性,一個用于存放字符串數據的char[], 一個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
...
}
因此,在關閉指針壓縮時,一個String對象的大小為:
Shallow Size=對象頭大小16字節+int類型大小4字節+數組引用大小8字節+padding4字節=32字節;
Retained Size=Shallow Size+char數組的Retained Size。
在開啟指針壓縮時,一個String對象的大小為:
Shallow Size=對象頭大小12字節+int類型大小4字節+數組引用大小4字節+padding4字節=24字節;
Retained Size=Shallow Size+char數組的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();
// 打印對象的shallow size
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(testObjectSize) + " bytes");
// 打印對象的 retained size
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(testObjectSize) + " bytes");
System.in.read();
}
}
開啟指針壓縮的情況
結果如下:
Shallow Size: 16 bytes
Retained Size: 64 bytes
MAT中的結果如下:
關閉指針壓縮的情況
結果如下:
Shallow Size: 24 bytes
Retained Size: 88 bytes
MAT中的結果如下:
其他引用類型的大小
根據上面的分析,可以計算出一個對象在內存中的占用空間大小情況,其他的引用類型可以參考分析計算過程來計算內存的占用情況。
關于padding
思考這樣一個問題,是不是padding都加到對象的后面呢,如果對象頭占12個字節,對象中只有1個long類型的變量,那么該long類型的變量的偏移起始地址是在12嗎?用下面一段代碼測試一下:
@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類來查看變量的偏移地址,運行后結果如下:
16
如果是換成int類型的變量呢?結果是12。
現在一般的CPU一次直接操作的數據可以到64位,也就是8個字節,那么字長就是64,而long類型本身就是占64位,如果這時偏移地址是12,那么需要分兩次讀取該數據,而如果偏移地址從16開始只需要通過一次讀取即可。int類型的數據占用4個字節,所以可以從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")));
}
}
運行結果如下:
16
12
13
在本例中,如果變量的大小小于等于4個字節,那么在分配內存的時候會先優先分配,因為這樣可以減少padding,比如這里的b和c變量;如果這時達到了16個字節,那么其他的變量按照類型所占內存的大小降序分配。
再次修改代碼:
/**
* 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));
}
}
結果如下:
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,因為它正好是4個字節,其余的都是先從g和h變量開始分配的,因為這兩個變量是long類型和double類型的,占64位,最后分配的是a和b,它們只占一個字節。
如果分配到最后,這時字節數不是8的倍數,則需要padding。這里實際的大小是42字節,所以padding6字節,最終占用48字節。