1. 使用Java 自帶的內存查看工具進行分析
對於如下代碼:
import java.util.ArrayList; import java.util.List; public class PlainTest { public static void main(String[] args) throws InterruptedException { List<EmptyObject> emptyObjects = new ArrayList<>(); for (int i = 0; i < 1000; i++) { emptyObjects.add(new EmptyObject()); } Thread.sleep(600 * 1000); } private static class EmptyObject { } }
我們啟動之后用jvisualvm 進行查看:

如下們可以看到每個對象占用16個字節。
2. 使用其他工具進行查看
使用jol 進行查看,jol git 地址: https://github.com/openjdk/jol
1. pom 引入
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
2. 代碼查看:
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws InterruptedException { System.out.println(ClassLayout.parseInstance(new EmptyObject()).toPrintable()); } // 1000個實例是16000字節,每個對象是16字節 private static class EmptyObject { } }
結果:
PlainTest$EmptyObject 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) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到每個對象占用的是16個字節,16字節存儲的內容是什么。
在HotSpot 虛擬機里,對象在堆內存中的存儲布局可以划分為以下三個部分:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)等。
對象頭包括兩部分信息:
第一類是存儲對象自身的運行時數據,比如hashcode、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。這部分的數據在32位和64位的虛擬機(未開啟壓縮指針)中分別為32個bit和64bit,官方稱之為Mark Word。
第二類是類型指針,即對象指向它的類型元數據的指針,Java 虛擬機通過這個指針來確定該對象是哪個類的實例。如果是數組,在對象頭中還必須有一塊記錄數組長度的數據,因為虛擬機可以通過普通Java對象的元數據信息確定Java 對象的大小,但是如果數組的長度是不確定的,將無法通過元數據中的信息推斷出數組的大小。
實例信息包括:對象存儲的真正的有效信息,也就是代碼中定義的各種類型的字段內容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。這部分的存儲順序受虛擬機分配策略參數和字段在Java源碼中定義順序的影響。
填充信息包括:這並不是必然存在的,也沒有特別的含義,它僅僅是起着占位符的作用。由於HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心的設計成正好是8字節的倍數(1倍或者2倍), 因此, 如果對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。
上面是開啟壓縮的情況,如果不開啟壓縮,記過如下:(啟動的時候指定壓縮參數關閉 -XX:-UseCompressedOops)
PlainTest$EmptyObject 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) f0 09 a9 19 (11110000 00001001 10101001 00011001) (430508528) 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
總結:
開啟指針壓縮: 對象頭被壓縮為12個byte, 所以需要4byte padding, 來構成8的倍數。
關閉指針壓縮: 對象頭是16byte已經是8的倍數了,不需要再padding。
3. 關於引用占用的字節數
在未壓縮的情況下,64位JVM中一個引用占用8byte;如果進行了壓縮一個引用占用4個byte。如下:
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new EmptyObject()).toPrintable()); } // 1000個實例是16000字節,每個對象是16字節 private static class EmptyObject { private String name = "123"; } }
結果:(啟動的時候指定壓縮參數關閉 -XX:-UseCompressedOops)
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) 28 0a 0c 19 (00101000 00001010 00001100 00011001) (420219432) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 8 java.lang.String EmptyObject.name (object) Instance size: 24 bytes
結果:(啟動的時候不指定壓縮參數,使用默認的開啟)
PlainTest$EmptyObject 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 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509) 12 4 java.lang.String EmptyObject.name (object) Instance size: 16 bytes
4. 數組對象
64位機器上,數組對象的對象頭占用24個字節,啟用壓縮之后占用16個字節。之所以比普通對象占用內存多是因為需要額外的空間存儲數組的長度。
(1) 例子1: 測試數組對象頭的大小
先考慮下new Integer[0]占用的內存大小,長度為0,即是對象頭的大小。
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new Integer[0]).toPrintable()); } }
結果:開啟壓縮 - 16bytes
[Ljava.lang.Integer; 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) 49 62 00 f8 (01001001 01100010 00000000 11111000) (-134192567) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 0 java.lang.Integer Integer;.<elements> N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
結果:關閉壓縮 - 24bytes
[Ljava.lang.Integer; 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) 80 ba 71 19 (10000000 10111010 01110001 00011001) (426883712) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 20 4 (alignment/padding gap) 24 0 java.lang.Integer Integer;.<elements> N/A Instance size: 24 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
(2) new Integer[3] 進行測試:
public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new Integer[3]).toPrintable()); }
關閉壓縮:結果 24(對象頭)+8*3=48 bytes (這里的8指的是引用大小,引用在未開啟壓縮的時候是8byte)
[Ljava.lang.Integer; 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) 80 ba 9b 19 (10000000 10111010 10011011 00011001) (429636224) 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 24 java.lang.Integer Integer;.<elements> N/A Instance size: 48 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
開啟壓縮:結果16(對象頭)+3*4 = 28 ,+ padding/4 = 32 (這里的4指的是引用大小,引用在開啟壓縮的時候是4byte)
[Ljava.lang.Integer; 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) 49 62 00 f8 (01001001 01100010 00000000 11111000) (-134192567) 12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3) 16 12 java.lang.Integer Integer;.<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
5. 復合對象
包括當前類及超類的基本類型實例字段大小、引用類型實例字段引用大小、實例基本類型數組總占用空間、實例引用類型數組引用本身占用空間大小; 但是不包括超類繼承下來的和當前類聲明的實例引用字段的對象本身的大小、實例引用數組引用的對象本身的大小。
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new ClassB()).toPrintable()); } private static class ClassA { // 引用類型 4 byte protected String name = "123"; // 基本數據類型long 8 byte protected long longVal = 1L; // 引用類型 4 byte protected Long longVal1 = 2L; // 數組引用4 byte protected int[] nums = {1, 2, 3}; // 基本類型 2 byte protected char charVal = 'c'; } // 1000個實例是16000字節,每個對象是16字節 private static class ClassB extends ClassA { // 基本數據類型long 8 byte protected long longVal2 = 3L; // 引用類型 4byte protected Long longVal12 = 4L; // 數組引用4 byte protected int[] nums2 = {4, 5, 6}; } }
結果: 開啟壓縮: (對象頭 12 + )
PlainTest$ClassB 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) 82 c0 00 f8 (10000010 11000000 00000000 11111000) (-134168446) 12 2 char ClassA.charVal c 14 2 (alignment/padding gap) 16 8 long ClassA.longVal 1 24 4 java.lang.String ClassA.name (object) 28 4 java.lang.Long ClassA.longVal1 2 32 4 int[] ClassA.nums [1, 2, 3] 36 4 java.lang.Long ClassB.longVal12 4 40 8 long ClassB.longVal2 3 48 4 int[] ClassB.nums2 [4, 5, 6] 52 4 (loss due to the next object alignment) Instance size: 56 bytes Space losses: 2 bytes internal + 4 bytes external = 6 bytes total
解釋:
(1) 開啟壓縮對象頭占12byte
(2) 基本類型char 占2byte,產生一個對象填充2byte,共4byte
(3) 基本類型long 占 8byte(共2是16byte)
(4) 引用類型占4byte(共5個是20byte)
總:12 + 4 + 16 + 20 = 52 byte, 不滿足8的倍數,因此產生4個字節的填充 = 56 byte
開啟壓縮之后結果如下:
PlainTest$ClassB 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) 38 12 76 19 (00111000 00010010 01110110 00011001) (427168312) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 8 long ClassA.longVal 1 24 2 char ClassA.charVal c 26 6 (alignment/padding gap) 32 8 java.lang.String ClassA.name (object) 40 8 java.lang.Long ClassA.longVal1 2 48 8 int[] ClassA.nums [1, 2, 3] 56 8 long ClassB.longVal2 3 64 8 java.lang.Long ClassB.longVal12 4 72 8 int[] ClassB.nums2 [4, 5, 6] Instance size: 80 bytes Space losses: 6 bytes internal + 0 bytes external = 6 bytes total
解釋:
(1) 開啟壓縮對象頭占16byte
(2) 基本類型char 占2byte,產生一個對象填充6byte,共8byte
(3) 基本類型long 占 8byte(共2是16byte)
(4) 引用類型占8byte(共5個是40byte)
總共是80byte
補充:alignment/padding 是間隙填充 , 可以理解為內部填充。 而最終的填充可以理解為外部填充。
alignment/padding 是間隙填充 。其產生條件是:對象的屬性中包含基本數據類型和引用數據類型,且基本數據類型的字節數和不是一個引用類型大小的整數倍,這時候會將基本類型填充為一個引用所占的大小(壓縮為4,不壓縮為8)。
比如:如下測試:
(1) 關閉壓縮: 只包含基本類型屬性
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new ClassB()).toPrintable()); } // 1000個實例是16000字節,每個對象是16字節 private static class ClassB { private byte byteVal = 1; } }
結果: 未產生間隙填充
PlainTest$ClassB 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) 18 0a 11 19 (00011000 00001010 00010001 00011001) (420547096) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 1 byte ClassB.byteVal 1 17 7 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
(2) 關閉壓縮,包含基本類型和引用類型
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new ClassB()).toPrintable()); } // 1000個實例是16000字節,每個對象是16字節 private static class ClassB { private byte byteVal = 1; private ClassB classB; } }
結果: 產生7byte的間隙
Disconnected from the target VM, address: '127.0.0.1:61359', transport: 'socket'
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) 28 0a eb 18 (00101000 00001010 11101011 00011000) (418056744)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 1 byte ClassB.byteVal 1
17 7 (alignment/padding gap)
24 8 PlainTest.ClassB ClassB.classB null
Instance size: 32 bytes
Space losses: 7 bytes internal + 0 bytes external = 7 bytes total
(3) 關閉壓縮,包含2個基本類型和引用類型
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new ClassB()).toPrintable()); } private static class ClassB { private int intVal = 1; private ClassB classB; private byte byteVal = 1; } }
結果: 產生3byte 間隙填充
PlainTest$ClassB 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 0a 0e 19 (01101000 00001010 00001110 00011001) (420350568) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 4 int ClassB.intVal 1 20 1 byte ClassB.byteVal 1 21 3 (alignment/padding gap) 24 8 PlainTest.ClassB ClassB.classB null Instance size: 32 bytes Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
(4) 開啟壓縮,用一個int 基本類型和引用類型測試
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new ClassB()).toPrintable()); } // 1000個實例是16000字節,每個對象是16字節 private static class ClassB { private int intVal = 1; private ClassB classB; } }
結果: 產生4byte的填充(外部填充)
PlainTest$ClassB 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 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509) 12 4 int ClassB.intVal 1 16 4 PlainTest.ClassB ClassB.classB null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
(5) 開啟壓縮,用一個int 基本類型、一個byte類型和引用類型測試
import org.openjdk.jol.info.ClassLayout; public class PlainTest { public static void main(String[] args) throws Exception { System.out.println(ClassLayout.parseInstance(new ClassB()).toPrintable()); } private static class ClassB { private int intVal = 1; private ClassB classB; private byte byteVal = 1; } }
結果: 產生3byte的間隙填充,也就是內部填充
PlainTest$ClassB 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 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509) 12 4 int ClassB.intVal 1 16 1 byte ClassB.byteVal 1 17 3 (alignment/padding gap) Disconnected from the target VM, address: '127.0.0.1:61854', transport: 'socket' 20 4 PlainTest.ClassB ClassB.classB null Instance size: 24 bytes Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
補充: 關於指針壓縮
在64位JVM中上有一個指針壓縮的概念,參數為-XX:+UseCompressedOops,默認是開啟的。如果先要關閉可以指定JVM啟動參數: -XX:-UseCompressedOops.
補充: 關於reference 類型的長度
Java 虛擬機並沒有明確規定reference 類型的長度,它的長度與實際使用32位還是64位虛擬機有關,如果是64位虛擬機,還與是否開啟某些對象指針壓縮等有關(開啟占用4byte,關閉占用8byte)。
