Java 对象的内存布局
Java的实例对象、数组对象在内存中的组成包括如下三部分:对象头Hearder、实例数据、内存填充。示意图如下所示
- 对象头:其主要包括两部分数据:Mark Word、Class对象指针。特别地对于数组对象而言,其还包括了数组长度数据。在64位的HotSpot虚拟机下,Mark Word占8个字节,其记录了Hash Code、GC信息、锁信息等相关信息;而Class对象指针则指向该实例的Class对象,在开启指针压缩的情况下占用4个字节,否则占8个字节;如果其是一个数组对象,则还需要4个字节用于记录数组长度信息。这里列出64位HotSpot虚拟机Mark Word的具体含义,以供参考。需要注意的是在下图的Mark Word中,左侧为高字节,右侧为低字节
- 实例数据:用于存放该对象的实例数据
- 内存填充:64位的HotSpot要求Java对象地址按8字节对齐,即每个对象所占内存的字节数必须是8字节的整数倍。因此Java对象需要通过内存填充来满足对齐要求(默认情况下,Java 虚拟机堆中对象的起始地址需要对齐至 8 的倍数,所有需要空白填充来使"内存对齐")
Note:
在64位的HotSpot虚拟机下,类型指针、引用类型需要占8个字节。显然这大大增加了内存的消耗和占用。为此从JDK 1.6开始,64位的JVM支持UseCompressedOops选项。其可对OOP(Ordinary Object Pointer,普通对象指针)进行压缩,使其只占用4个字节,以达到节约内存的目的。在JDK 8下,该选项默认启用。当然也可以通过添加JVM参数来显式进行配置:
1 -XX:+UseCompressedOops // 开启指针压缩 2 -XX:-UseCompressedOops // 关闭指针压缩
Java数据类型有哪些
- 基础数据类型(primitive type)
- 引用类型 (reference type)
基础数据类型内存占用如下
引用类型内存占用如下
引用类型跟基础数据类型不一样,除了对象本身之外,还存在一个指向它的引用(指针),指针占用的内存在64位虚拟机上8个字节,如果开启指针压缩是4个字节,默认是开启了的。
字段重排序
为了更高效的使用内存,实例数据字段将会重排序。排序的优先级为: long = double > int = float > char = short > byte > boolean > object reference
如下所示的类
1 class FieldTest{ 2 byte a; 3 int c; 4 boolean d; 5 long e; 6 Object f; 7 }
将会重排序为:
1 OFFSET SIZE TYPE DESCRIPTION 2 16 8 long FieldTest.e 3 24 4 int FieldTest.c 4 28 1 byte FieldTest.a 5 29 1 boolean FieldTest.d 6 30 2 (alignment/padding gap) 7 32 8 java.lang.Object FieldTest.f
PS:在后文使用JOL工具测试时,好像也不一定会按照这个顺序来,感觉是会遵循一个最优的排序,也就是使得排序后占用内存最小,不是很确定,备注一下。
实践
利用JOL工具分析很简单,首先在POM添加添加其Maven依赖
1 <!-- JOL依赖 --> 2 <dependency> 3 <groupId>org.openjdk.jol</groupId> 4 <artifactId>jol-core</artifactId> 5 <version>0.9</version> 6 </dependency>
例1:类对象占用大小
1 package com.study; 2 3 import org.openjdk.jol.info.ClassLayout; 4 5 class Test{ 6 private long orderId; 7 private byte state; 8 private long createMillis; 9 private char c; 10 private int i; 11 private double d; 12 private long userId; 13 } 14 15 16 public class Test06 { 17 public static void main(String[] args) { 18 System.out.print(ClassLayout.parseClass(Test.class).toPrintable()); 19 } 20 }
结果如下:
例2:实例对象占用内存大小
1 package com.study; 2 3 import lombok.Builder; 4 import lombok.Data; 5 import org.openjdk.jol.info.ClassLayout; 6 7 @Data 8 @Builder 9 class Car { 10 private int id; 11 private String type; 12 private double price; 13 private char level; 14 } 15 16 public class JOLDemo { 17 public static void main(String[] args) { 18 Car car = Car.builder() 19 .id(1) 20 .type("SUV") 21 .level('A') 22 .price(22.22) 23 .build(); 24 25 System.out.println(ClassLayout.parseInstance(car).toPrintable()); 26 27 int[] array = new int[3]; 28 array[0] = 11; 29 array[1] = 22; 30 array[2] = 33; 31 System.out.println(ClassLayout.parseInstance(array).toPrintable()); 32 } 33 }
结果是:
如果设置了JVM选项-XX:-UseCompressedOops以关闭指针压缩。下面即是Java对象的内存布局信息输出及相关分析结果,这里笔者的CPU主机字节序为小端
例3:父类的私有成员变量是否会被子类继承?
1 package com.study; 2 3 import org.openjdk.jol.info.ClassLayout; 4 5 class Fruit { 6 private int size; 7 public String name; 8 } 9 10 class Apple extends Fruit { 11 private int size; 12 private String name; 13 private Apple brother; 14 private long create_time; 15 } 16 17 18 public class JOLDemo { 19 public static void main(String[] args) { 20 System.out.println(ClassLayout.parseClass(Fruit.class).toPrintable()); 21 System.out.println(ClassLayout.parseClass(Apple.class).toPrintable()); 22 } 23 }
结果如图:
总结
字段重排列,顾名思义,就是 Java 虚拟机重新排列字段的在内存中的顺序,以达到内存利用率最大,即减少内存填充。Java 虚拟机中有三种排列方法(对应 Java 虚拟机选项 -XX:FieldsAllocationStyle,默认值为 1),但都会遵循如下两个规则。
- 其一、如果一个字段占据 C 个字节,那么该字段的偏移量需要对齐至 NC。这里偏移量指的是字段地址与对象的起始地址差值。
- 其二,子类所继承字段的偏移量,需要与父类对应字段的偏移量保持一致