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)。