对象布局
1. 简介
new一个对象会保存在堆内存-->新生代-->eden区,在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
2. 对象组成
3. 对象头
3.1 对象标记
对象标记Mark Word(openJdk中称为mark oop),里面保存哈希码,GC标记,GC次数,同步锁标记,偏向锁持有者。下图为标志位值对应的状态信息: 在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节,下图为各个占用情况:
默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。 下面是不同锁的对象标记:
3.2 类元信息
类元信息(又叫类型指针openJdk中称为kclass oop),指向保存在元空间中Klass
3.3 对象头大小
对象标记大小和类元信息指针大小的和。
4. 实例数据
存放类的属性(Field)数据信息,包括父类的属性信息
5. 对齐填充
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐
6. 源码
在oop.hpp中定义了对象结构: 对象结构中占用字节数分布情况:
其中
- hash: 保存对象的哈希码。
- age: 保存对象的分代年龄。
- biased_lock: 偏向锁标识。
- lock: 锁状态标识位。
- JavaThread*: 保持偏向锁的线程。
- epoch: 保存偏向锁的时间戳。
7. 使用JOL
JOL工具是openjdk官方提供的分析JVM中对象的大小和布局的工具:
7.1 添加依赖
在项目中pom.xml添加:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
7.2 代码编写
创建ObjectMemoryDemo.java:
public class ObjectMemoryDemo {
static class MyNumber{
int number = 0;
boolean flag = true;
}
public static void main(String[] args) throws InterruptedException {
System.out.println(VM.current().details());
System.out.println(VM.current().objectAlignment());
}
}
运行的时候,加上JVM参数-Djdk.attach.allowAttachSelf
: 其中OFFSET:偏移量,也就是到这个字段位置所占用的bytes数
SIZE: 后面类型的字节大小
TYPE: Class中定义的类型
压缩指针简介
压缩指针(Compressed OOPs,即Compressed Ordinary Object Pointers)是Java虚拟机中一项重要的内存优化技术,用于减少64位JVM中对象指针占用的内存空间,从而提高内存利用率和GC性能。
64位JVM要求对象起始地址必须是8字节对齐(即地址末3位为0),压缩指针存储实际地址除以8后的商(相当于右移3位),例如:
实际地址:0x0000000000001000(十进制4096);
压缩后指针:0x00000100(4096÷8=500)。
当64位JVM使用压缩指针访问对象时,会自动将指针左移3位(乘以8)还原为实际地址。
压缩指针仅在堆内存小于32GB时有效。若堆超过32GB,指针会自动变为8字节(即禁用压缩指针)。
如果我们打印MyNumber对象:
public static void main(String[] args) throws InterruptedException {
MyNumber myNumber = new MyNumber();
System.out.println(ClassLayout.parseInstance(myNumber).toPrintable());
}
执行结果: 如果此时我们删除MyNumber对象中的所有属性:
public class ObjectMemoryDemo {
static class MyNumber{
}
public static void main(String[] args) throws InterruptedException {
MyNumber myNumber = new MyNumber();
System.out.println(ClassLayout.parseInstance(myNumber).toPrintable());
}
}
打印结果: 可以发现类型指针也就是类元信息只占用4个字节,和最开始说的类型指针占了8个字节不符合,这是因为这里JVM使用压缩指针做了优化,默认是开启的,可以使用JVM参数:
XX:+UseCompressedClassPointers
来控制行为。使用-XX:-UseCompressedClassPointers
将+
变成-
即表示关闭压缩指针。
8. 分代年龄
JavaGC的时候,是在幸存者区回收第15次后,如果该对象还在就会被转移到永久代中,为何定义的分代年龄必须是15呢?
因为在对象头中分代年龄只有4个字节,最大就是15,如果需要自定义的话,JVM传入参数:-XX:MaxTenuringThreshold=32
如果设置为16,由于4个字节大小永远不会达到16,等于设置了永远不根据分代年龄进行晋升。