写java时不管是我们自己new对象还是spring管理bean,尽管我们天天跟对象打交道,那么对象的结构和内存布局有多少人知道呢,这篇文章可带你入门,了解java对象内存布局。
本文涉及到JVM指针压缩的知识点,不熟悉的小伙伴可以看前面写过的一篇关于指针压缩的文章。
JVM之指针压缩

首先说明,本文涉及的JDK版本是1.8,JVM虚拟机是64位的HotSpot实现为准。

java对象结构

关于java对象我们知道, 对象的实例是存在堆上的, 对象的元数据存在方法区(元空间)上,对象的引用保存在栈上的。那么java对象的结构是什么样的呢,其实java对象由三部分构成。

  1. 对象头

对象头里也有三部分构成。

  • Markword

存储对象的hashCode、垃圾回收对象的年龄以及锁信息等。

  • 类型指针

对象指向的类信息地址即元数据指针,比如User对象指针指向User.class的JVM内存地址。注意:jdk1.8以后元数据是存在Metaspace里的,jdk1.8之前是在方法区里

  • 数组长度

只有对象是数组的情况下,才有这部分数据,若对象不是数组,则没有这部分,不分配空间。

  1. 对象体

对象里的非静态属性占用的空间(包括父类的所有属性,不区分修饰类型),不包括方法,注意:是非静态属性,属于对象的属性,静态属性是属于类的不在对象上分配空间。如果属性是基本数据类型,则直接存对象本身,如果是引用类型,则存的是对象的指针。

  1. 对齐填充

默认情况下,如果对象头+对象体大小不是8字节的倍数,则通过该部分进行补齐,比如对象头+对象体大小只有30字节,则需要补齐到32字节,这里的对齐填充就是2字节。默认情况下,JVM中对象是以8字节对齐的,若对象头加上对象体是8的倍数时,则不存在字节对齐,否则会填充补齐到8的倍数。
对象结构如下图所示。

通过图中可以看出,数组对象只是在对象头里多了数组长度这一项,普通对象(非数组对象)没有这项,也不分配内存空间。
对象结构及占用空间大小如下图所示。

涉及指针压缩的地方有两个,一个是对象头里的类型指针,一个是对象体里的引用类型指针,这篇文章里有详细的介绍:JVM之指针压缩。

对象头

对象头包含三部分

  • Markword:存储对象自身运行时数据如hashcode、gc分代年龄及锁信息等,64位系统总共占用8个字节。
  • 类型指针:对象指向类元数据地址的指针,jdk8默认开启指针压缩,64位系统占4个字节
  • 数组长度:若对象不是数组,则没有该部分,不分配空间大小,若是数组,则为4个字节长度

对象头占用空间大小如下表所示。

Markword

存储对象自身运行时数据如hashcode、gc分代年龄及锁信息等,64位系统总共占用8个字节,也就是64bit,64位的二进制0和1。

解释如下:

  • 对象的hashCode占31位,重写类的hashCode方法返回int类型,只有在无锁情况下,是在有调用的情况下会计算该值并写到对象头中,其他情况该值是空的。
  • 分代年龄占4位,最大值也就是15,在GC中,当survivor区中对象复制一次,年龄加1,默认是到15之后会移动到老年代。
  • 是否偏向锁占1位,无锁和偏向锁的最后两位都是01,使用这一位来标识区分是无锁还是偏向锁。
  • 锁标志位占2位,锁状态标记位,同是否偏向锁标志位标识对象处于什么锁状态。
  • 偏向线程ID占54位,只有偏向锁状态才有,这个ID是操作系统层面的线程唯一id,跟java中的线程id是不一致的。

类型指针

类型指针指向类的元数据地址,JVM通过这个指针确定对象是哪个类的实例。32位的JVM占32位,4个字节,64位的JVM占64位,8个字节,但是64位的JVM默认会开启指针压缩,压缩后也只占4字节。
64位虚拟机中在堆内存小于32GB的情况下,UseCompressedOops是默认开启的,该参数表示开启指针压缩,会将原来64位的指针压缩为32位。
-XX:+UseCompressedClassPointers //开启压缩类指针
-XX:-UseCompressedClassPointers //关闭压缩类指针

这个JVM参数依赖UseCompressedOops这个参数,UseCompressedOops开启,UseCompressedClassPointers默认开启,可手工关闭,UseCompressedOops关闭,UseCompressedClassPointers不管开启还是关闭都不生效即不压缩。

数组长度

如果对象是普通对象非数组对象,则没有这部分,不占用空间。
如果对象是一个数组,则将数组的长度存到对象头里,表示数组的大小。

对象体

对象体里放的是非静态的属性,也包括父类的所有非静态属性(private修饰的也在这里,不区分可见性修饰符),基本类型的属性存放的是具体的值,引用类型及数组类型存放的是引用指针。

对齐填充

虚拟机为了高效寻址,采用8字节对齐,所以对象大小不是8的倍数时,会补齐对应的位置,比如对象头+对象体是32字节时,则不需要对齐填充,对象头+对象体是12字节时,则需补齐4位。

对象大小的计算

对象的大小跟指针压缩是否开启有关,可通过以下两个参数控制。
UseCompressedClassPointers:压缩类指针(开启时类指针占4字节,关闭时类指针占8字节)
UseCompressedOops:压缩普通对象指针(开启时引用对象指针占4字节,关闭时引用对象指针占8字节)
这两个参数默认是开启的,即-XX:+UseCompressedClassPointers,-XX:+UseCompressedOops,也可手动设置,如下所示

-XX:+UseCompressedClassPointers //开启压缩类指针-XX:-UseCompressedClassPointers //关闭压缩类指针-XX:+UseCompressedOops  //开启压缩普通对象指针-XX:-UseCompressedOops  //关闭压缩普通对象指针

32位HotSpot VM是不支持UseCompressedOops参数的,只有64位HotSpot VM才支持。
Oracle JDK从6 update 23开始在64位系统上会默认开启压缩指针。

以下表格展示了对象中各部分所占空间大小,单位:字节。

类型所属部分占用空间大小(压缩开启)占用空间大小(压缩关闭)
Markwork对象头88
类型指针对象头48
数组长度对象头44
byte对象体11
boolean对象体11
short对象体22
char对象体22
int对象体44
float对象体44
long对象体88
double对象体88
对象引用指针对象体48
对齐填充对齐填充对象头+对象体是8的倍数?0 :8 -(对象头+对象体)% 8对象头+对象体是8的倍数?0 :8 -(对象头+对象体)% 8

对象大小计算公式
对象大小=对象头 + 对象体(对象是数组时,对象体的大小=引用指针占用空间大小*对象个数) + 对齐填充
64位操作系统32G内存以下,默认开启对象指针压缩,对象头是12字节,关闭指针压缩,对象头是16字节。内存超过32G时,则自动关闭指针压缩,对象头占16字节。

对象分析

有了以上的理论知识,我们通过实际案例进行对象分析。
使用 JOL 工具分析 Java 对象大小
maven依赖

    org.openjdk.jol    jol-core    0.17

常用类及方法
查看对象内部信息:ClassLayout.parseInstance(obj).toPrintable()
查看对象外部信息:GraphLayout.parseInstance(obj).toPrintable()
查看对象占用空间总大小:GraphLayout.parseInstance(obj).totalSize()
查看类内部信息:ClassLayout.parseClass(Object.class).toPrintable()

使用到的测试类:

@Setterclass Goods {    private byte b;    private char type;    private short age;    private int no;    private float weight;    private double price;    private long id;    private boolean flag;    private String goodsName;    private LocalDateTime produceTime;    private String[] tags;    public static String str;    public static int temp;}

非数组对象,开启指针压缩

64位JVM,堆内存小于32G的情况下,默认是开启指针压缩的。

public static void main(String[] args) {    Goods goods = new Goods();    goods.setAge((short) 10);    goods.setNo(123456);    goods.setId(111L);    goods.setGoodsName("方便面");    goods.setFlag(true);    goods.setB((byte)1);    goods.setPrice(1.5d);    goods.setProduceTime(LocalDateTime.now());    goods.setType('A');    goods.setWeight(0.065f);    goods.setTags(new String[] {"food", "convenience", "cheap"});    Goods.str = "test";    Goods.temp = 222;    System.out.println(ClassLayout.parseInstance(goods).toPrintable());}

计算对象大小:
先不看输出结果,按上面的公式计算一下对象的大小:
对象头:8字节(Markword)+4字节(类指针)=12字节
对象体:1字节(属性b)+ 2字节(属性type)+ 2字节(属性age)+ 4字节(属性no)+ 4字节(属性weight)+ 8字节(属性price)+ 8字节(属性id)+ 1字节(属性flag) + 4字节(属性goodsName指针) + 4字节(属性produceTime指针) + 4字节(属性tags指针)= 42字节(注意:静态属性不参与对象大小计算)
对齐填充:8 -(对象头+对象体)% 8 = 8 – (12 + 42) % 8 = 2字节
对象大小=对象头 + 对象体 + 对齐填充 = 12字节 + 42字节 + 2字节 = 56字节。
执行看运行结果:

com.star95.study.jvm.Goods object internals:OFF  SZ                      TYPE DESCRIPTION               VALUE  0   8                           (object header: mark)     0x0000000000000001 (non-biasable; age: 0)  8   4                           (object header: class)    0x2000c043 12   4                       int Goods.no                  123456 16   8                    double Goods.price               1.5 24   8                      long Goods.id                  111 32   4                     float Goods.weight              0.065 36   2                      char Goods.type                A 38   2                     short Goods.age                 10 40   1                      byte Goods.b                   1 41   1                   boolean Goods.flag                true 42   2                           (alignment/padding gap)    44   4          java.lang.String Goods.goodsName           (object) 48   4   java.time.LocalDateTime Goods.produceTime         (object) 52   4        java.lang.String[] Goods.tags                [(object), (object), (object)]Instance size: 56 bytesSpace losses: 2 bytes internal + 0 bytes external = 2 bytes total

这里有一个特殊的地方,打印输出的属性顺序跟代码里的顺序不一致,这是因为JVM进行优化,也就是指令重排序,会根据属性类型的大小、执行的先后顺序对结果是否有影响、最小填充大小等因素计算出对象最小应占用的空间。

非数组对象,关闭指针压缩

计算对象大小:
关闭压缩指针,类指针和引用对象指针都占8字节,推算一下对象大小:
对象头:8字节(Markword)+8字节(类指针)=16字节
对象体:1字节(属性b)+ 2字节(属性type)+ 2字节(属性age)+ 4字节(属性no)+ 4字节(属性weight)+ 8字节(属性price)+ 8字节(属性id)+ 1字节(属性flag) + 8字节(属性goodsName指针) + 8字节(属性produceTime指针) + 8字节(属性tags指针)= 54字节(注意:静态属性不参与对象大小计算)
对齐填充:8 -(对象头+对象体)% 8 = 8 – (16 + 54) % 8 = 2字节
对象大小=对象头 + 对象体 + 对齐填充 = 16字节 + 54字节 + 2字节 = 72字节。
运行时增加JVM参数如下:

-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class ObjectLayOut1 {    public static void main(String[] args) {        Goods goods = new Goods();        goods.setAge((short) 10);        goods.setNo(123456);        goods.setId(111L);        goods.setGoodsName("方便面");        goods.setFlag(true);        goods.setB((byte)1);        goods.setPrice(1.5d);        goods.setProduceTime(LocalDateTime.now());        goods.setType('A');        goods.setWeight(0.065f);        goods.setTags(new String[] {"food", "convenience", "cheap"});        Goods.str = "test";        Goods.temp = 222;        System.out.println(ClassLayout.parseInstance(goods).toPrintable());    }}

执行看运行结果:

com.star95.study.jvm.Goods object internals:OFF  SZ                      TYPE DESCRIPTION               VALUE  0   8                           (object header: mark)     0x0000000000000001 (non-biasable; age: 0)  8   8                           (object header: class)    0x00000000175647b8 16   8                    double Goods.price               1.5 24   8                      long Goods.id                  111 32   4                       int Goods.no                  123456 36   4                     float Goods.weight              0.065 40   2                      char Goods.type                A 42   2                     short Goods.age                 10 44   1                      byte Goods.b                   1 45   1                   boolean Goods.flag                true 46   2                           (alignment/padding gap)    48   8          java.lang.String Goods.goodsName           (object) 56   8   java.time.LocalDateTime Goods.produceTime         (object) 64   8        java.lang.String[] Goods.tags                [(object), (object), (object)]Instance size: 72 bytesSpace losses: 2 bytes internal + 0 bytes external = 2 bytes total

数组对象开启指针压缩

计算对象大小:
默认是开启压缩指针的,类指针和引用对象指针都占4字节,推算一下对象大小:
对象头:8字节(Markword)+ 4字节(类指针) + 4字节(数组长度)= 16字节
对象体:4字节 * 3 = 12字节
对齐填充:8 -(对象头+对象体)% 8 = 8 – (16字节 + 12字节)% 8= 4字节
对象大小=对象头 + 对象体 + 对齐填充 = 16字节 + 12字节 + 4字节 = 32字节。

public class ObjectLayOut1 {    public static void main(String[] args) {        Goods goods = new Goods();        goods.setAge((short) 10);        goods.setNo(123456);        goods.setId(111L);        goods.setGoodsName("方便面");        goods.setFlag(true);        goods.setB((byte)1);        goods.setPrice(1.5d);        goods.setProduceTime(LocalDateTime.now());        goods.setType('A');        goods.setWeight(0.065f);        goods.setTags(new String[] {"food", "convenience", "cheap"});        Goods.str = "test";        Goods.temp = 222;        Goods[] goodsArr = new Goods[3];        goodsArr[0] = goods;        System.out.println(ClassLayout.parseInstance(goodsArr).toPrintable());    }}

执行看运行结果:

[Lcom.star95.study.jvm.Goods; object internals:OFF  SZ                         TYPE DESCRIPTION               VALUE  0   8                              (object header: mark)     0x0000000000000001 (non-biasable; age: 0)  8   4                              (object header: class)    0x2000c18d 12   4                              (array length)            3 16  12   com.star95.study.jvm.Goods Goods;.         N/A 28   4                              (object alignment gap)    Instance size: 32 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

数组对象关闭指针压缩

计算对象大小:
关闭压缩指针,类指针和引用对象指针都占8字节,推算一下对象大小:
对象头:8字节(Markword)+8字节(类指针) + 4字节(数组长度)=20字节
对象体:8字节 * 3 = 24字节
对齐填充:8 -(对象头+对象体)% 8 = 8 – (20+ 24) % 8 = 4字节
对象大小=对象头 + 对象体 + 对齐填充 = 20字节 + 24字节 + 4字节 = 48字节。
运行时增加JVM参数如下:

-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class ObjectLayOut1 {    public static void main(String[] args) {        Goods goods = new Goods();        goods.setAge((short) 10);        goods.setNo(123456);        goods.setId(111L);        goods.setGoodsName("方便面");        goods.setFlag(true);        goods.setB((byte)1);        goods.setPrice(1.5d);        goods.setProduceTime(LocalDateTime.now());        goods.setType('A');        goods.setWeight(0.065f);        goods.setTags(new String[] {"food", "convenience", "cheap"});        Goods.str = "test";        Goods.temp = 222;        Goods[] goodsArr = new Goods[3];        goodsArr[0] = goods;        System.out.println(ClassLayout.parseInstance(goodsArr).toPrintable());    }}

执行看运行结果:

[Lcom.star95.study.jvm.Goods; object internals:OFF  SZ                         TYPE DESCRIPTION               VALUE  0   8                              (object header: mark)     0x0000000000000001 (non-biasable; age: 0)  8   8                              (object header: class)    0x0000000017e04d70 16   4                              (array length)            3 20   4                              (alignment/padding gap)    24  24   com.star95.study.jvm.Goods Goods;.         N/AInstance size: 48 bytesSpace losses: 4 bytes internal + 0 bytes external = 4 bytes total

通过以上对象分析,我们看到在开启压缩指针的情况下,对象的大小会小很多,节省了内存空间。

总结

通过以上的分析,基本已经把java对象的结构讲清楚了,另外对象占用内存空间大小也计算出来,有助于进行JVM调优分析,64位的虚拟机内存在32G以下时默认是开启压缩指针的,超过32G自动关闭压缩指针,主要目的都是为了提高寻址效率。
另外,本文是通过JOL工具计算对象占用空间的大小,不包括引用对象实际占用的内存大小,因为计算时是按引用对象的指针占用空间大小计算的,可能跟其他工具计算的结果不一样,具体跟工具的计算逻辑有关,比如跟JDK自带的jvisualvm工具通过堆dump出来看到的对象大小不一样,感兴趣的可自行验证。