暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

浅析Java对象结构以及指针压缩

Just do DT 2021-11-24
1074

HotSpot 中的 Java 对象布局

在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。


且 Java 对象实例的大小是不可变的,所以知道确定了类型就可以知道对象实例的大小。


OOP

ordinary object pointer,也有说法是 object oriented pointer。

OOP与klass


JOL 工具简介

在具体开始研究对象的内存结构之前,先介绍一下我们要用到的工具,openjdk 官网提供了查看对象内存布局的工具 jol(java object layout),可以在 maven 中引入坐标使用。

<dependency> 
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>

在代码中使用 jol 提供的方法查看 jvm 信息:

package com.justdodt.jvm;


import org.openjdk.jol.vm.VM;


/**
* @Author:JustDoDT
* @Description:
* @Date:Create in 21:49 2021/11/21
* @Modified By:
*/


public class JvmInfo {
public static void main(String[] args) {
System.out.println(VM.current().details());
}
}

运行结果为:


通过打印出来的信息,可以看到我们使用的是 64位 jvm,并开启了指针压缩,对象默认使用 8 字节对齐的方式。通过 jol 查看对象内存布局的方法,将在后面例子具体展开。

对象头(Header)

HotSpot 虚拟机的对象头包含两部分信息:Mark Word 和 类型指针;如果是数组对象的话,还有第三部分信息:数组长度。

对象头示例

package com.justdodt.jvm;


import org.openjdk.jol.info.ClassLayout;


/**
* @Author:JustDoDT
* @Description:
* @Date:Create in 22:11 2021/11/21
* @Modified By:
*/


public class JvmClassLayout {
public static void main(String[] args) throws Exception{
JvmClassLayout jvmClassLayout = new JvmClassLayout();
//查看对象的内存布局
System.out.println(ClassLayout.parseInstance(jvmClassLayout).toPrintable());
}
}

执行代码,查看输出结果:


  • OFFSET:偏移地址,单位为字节
  • SIZE:占用内存大小,单位为字节
  • TYPE:Class 中定义的类型
  • DESCRIPTION:类型描述,Object header 表示对象头,alignment 表示对齐填充
  • VALUE:对应内存中存储的值
当前对象占用 16 字节,因为 8 字节标记字加 4 字节的类型指针,不满足向 8 字节对齐,因此需要填充 4 个字节:
8B (mark word) + 4B (klass pointer) + 0B (instance data) + 4B (padding)

上面的示例,我们了解了一个不包含属性的最简单的空对象,在内存中的基本组成是怎样的。

Mark Word

这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit和64bit。Mark Word 用于存储对象自身的运行时数据,如哈希码(Hash Code)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。对象头信息是与对象定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被涉及成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,她会根据对象的状态复用自己的存储空间。



标志位“01”就被复用了,根据不同的状态:“未锁定” or “可偏向” 来确定“01”存储所表示的内容。

类型指针(Class Pointer)

是对象指向她的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。Klass Pointer 是一个指向方法区中 Class 信息的指针,虚拟机通过这个指针确定该对象属于哪个类的实例。在JVM 中支持指针压缩功能,根据是否开启指针压缩,Klass pointer 占用的大小将会不同:
  • 未开启指针压缩时,类型指针占用 8B(64bit)
  • 开启指针压缩情况下,类型指针占用4B(32bit)
在 jdk6 之后的版本中,指针压缩是被默认开启的,可通过启动参数开启或关闭该功能:
#开启指针压缩:
-XX:+UseCompressedOops
#关闭指针压缩:
-XX:-UseCompressedOops

指针压缩原理

在了解了指针压缩的作用后,那么指针压缩是如何实现的呢?首先在不开启指针压缩的情况下,一个对象的内存地址使用 64 位表示,这时能描述的内存地址范围是:
0~2^64-1


在开启指针压缩后,使用 4个字节也就是32位,可以表示 2^32个内存地址,如果这个地址是真实地址的话,由于 CPU 寻址的最小单位是 Byte,那么就是4GB内存。这对于我们来说是远远不够的,但是之前我们说过,java 中对象默认使用了8字节对齐,也就是说1个对象占用的空间必须是8字节的整数倍,这样就创造了一个条件,使 jvm 在定位一个对象时不需要使用真正的内存地址,而是定位到由 java 进行了8字节映射后的地址(可以说是一个映射地址的编号)。
映射过程也非常简单,由于使用了8字节对齐后每个对象的地址偏移量后3位必定为0,所以在存储的时候可以将后3位0抹除(转化为bit是抹除了最后24位),在此基础上再去掉最高位,就完成了指针从8字节到4字节的压缩。而在实际使用时,在压缩后的指针后加3位0,就能够实现向真实地址的映射。
完成压缩后,现在指针的32位中的每一个bit,都可以代表8个字节,这样就相当于使原有的内存地址得到了8倍的扩容。所以在8字节对齐的情况下,32位最大能表示2^32*8=32GB内存,内存地址范围是:
0 ~ (2^32-1)*8


为什么要引入压缩指针

首先,在32位的操作系统可以寻址到的内存是 2^32=4GB。那么64位操作系统可以寻址的范围是2^64,这个值已经非常大了。如果用32位操作系统的话,我们很多场景内存不够用,比如,你用8GB的内存在32位电脑上只有4GB是有效的,而4GB又无法满足我们的需求。但是64位又过长,给我们的寻址带宽和同一个对象在堆中需要更多的存储空间。

数组长度(Length)[option]

如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组的长度。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。

实例数据(Instance Data)

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中分配一个非static的字段在前面字段缝隙中。这么做也是为了提高内存的利用率。
且实例数据主要由于 -XX:+CompactFields(JDK 8下默认为启用) 和 -XX:FieldsAllocationStyle 参数控制。
-XX:FieldsAllocationStyle=1 (JDK 8下默认值为‘1’)
对象在内存中的布局首要相关配置就是 FieldsAllocationStyle ,这个配置有 3个 值,分别是 0,1,2。当值为 2的时候,会经过一些逻辑判断最终转化为 0 或者 1。
  • -XX:FieldsAllocationStyle=0,表示先分配对象,然后再按照 double/long/ints、shorts/chars、bytes/booleans的顺序分配其他字段,也就是类中声明的相同宽度的字段总是会被分配在一起,而相同宽度字段的顺序则是他们在 class 文件中声明的顺序。
  • -XX:FieldsAllocationStyle=1(默认值),表示先按照double/long、ints、chars/shorts、bytes/booleans的顺序分配属性,然后再分配对象,分配过程中的其他原则上面为0时是保持一致的。
上面这2种分配策略只是针对大部分正常情况而言,有如下几种情况是会有所区别的。
  • 如果是特定的类,例如基本类型的包装类、String、Class、ClassLoader、软引用等类,会先分配对象,然后再按照 double/long、ints、chars/shorts、bytes/booleans的顺序分配,同时-XX:+CompactFields和-XX:FieldsAllocationStyle=1都不会生效。
  • 如果配置-XX:+CompactFields,会将ints、shorts/chars、bytes/booleans、oops的顺序将字段填充到对象头信息与字段起始偏移位置的间隙中去
  • 如果当前类或者类中使用了注解@sun.misc.Contended, 也会打乱上述布局
-XX:CompactFields
-XX:CompactFields 表示是否将对象中较窄的数据插入到间隙中。

对齐填充(Padding)

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。

对象头占用内存大小

上面已经分别对对象在内存的布局有了一点儿了解,接下来我们看看对象占用内存的大小。也就是对象内存结构的每个部分分别占用多少的内存。

对象头占用的内存


在 64 位的系统中占用 8 个字节:



普通对象占用内存情况:



数组对象占用内存情况:



实例数据占用内存情况:



案例分析1

涉及 JVM 参数

-XX:+UseCompressedOops(JDK8下默认启用)

package com.justdodt.jvm;


import org.openjdk.jol.info.ClassLayout;


/**
* @Author:JustDoDT
* @Description:
* @Date:Create in 22:11 2021/11/21
* @Modified By:
*/


public class JvmClassLayout {
int i;
byte b;
String str;
public static void main(String[] args) throws Exception{
JvmClassLayout jvmClassLayout = new JvmClassLayout();
//查看对象的内存布局
System.out.println(ClassLayout.parseInstance(jvmClassLayout).toPrintable());
}
}



输出结果如下:



当不开启 UseCompressedOops 时候,输出结果为:




案例分析2

package com.justdodt.jvm;


import org.openjdk.jol.info.ClassLayout;


public class JvmClassLayout {
int i;
byte b;
public static void main(String[] args) throws Exception{
JvmClassLayout jvmClassLayout = new JvmClassLayout();
//查看对象的内存布局
System.out.println(ClassLayout.parseInstance(jvmClassLayout).toPrintable());
}
}



当设置 -Xmx32g -XX:+UseCompressedOops ,输出结果为:



由此可以看出,堆内存超过了压缩指针的最大值。且指针压缩失效,指针长度恢复到8字节。

当设置 -Xmx32g -XX:-UseCompressedOops ,输出结果为:



当设置 -Xmx32g -XX:+UseCompressedOops -XX:ObjectAlignmentInBytes=16时候,输出结果为:


其中,-XX:ObjectAlignmentInBytes=16 此参数表示以 16 字节进行填充对齐。按照上面的计算,这时配置最大堆内存为64GB时候指针压缩才会失效。
指针压缩的总结:
  • 通过指针压缩,利用对齐填充特性,通过映射方式达到了内存地址扩展的效果
  • 指针压缩能够节省内存空间,同时提高了程序的寻址效率
  • 堆内存设置最好不要超过32GB,这时指针压缩将会失效,造成空间的浪费
  • 指针压缩不仅可以作用于对象头的类型指针,还可以作用于引用类型的字段指针,以及引用类型数组指针

参考


https://wiki.openjdk.java.net/display/HotSpot/CompressedOops

https://blog.csdn.net/qq_34212276/article/details/117914322


文章转载自Just do DT,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论