对象的内存布局

3 篇文章 1 订阅
文章详细介绍了JVM中对象在内存中的布局,包括对象头(MarkWord和ClassPointer)、实例数据和对齐填充。讨论了锁的状态,如无锁、偏向锁和重量级锁在MarkWord中的表示,并通过示例展示了如何观察这些信息。此外,还提到了实例数据的存储规则和内存对齐的重要性。
摘要由CSDN通过智能技术生成

介绍

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充

实例数据:存放类的属性数据信息,包括父类的属性信息;

对齐填充:仅仅是为了字节对齐,虚拟机要求对象起始地址必须是8字节的整数倍。填充的数据不是必须存在的;

对象头:Java对象头一般占有2个机器码,但是如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

▪ 虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)

  • ClassPointer是对象指向类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

  • MarkWord则是用于存储对象自身的运行时数据,比如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。

Mark Word

引入 jol-core 依赖

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

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

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

通过打印出来的信息,可以看到我们使用的是64位 jvm,并开启了指针压缩,对象默认使用8字节对齐方式。

    public static void main(String[] args) {

        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());


    }

对象内存输出结果:

上图所示中的1,2,3分别对应了:

  • Mark Word 8字节

  • 类型指针

  • 对齐填充: 因为前面两部分 相加为8+4 = 12, 要是最后时8的倍数,所以补上4个字节

其余展示含义:

  • OFFSET:偏移地址,单位为字节

  • SIZE:占用内存大小,单位为字节

  • TYPE:Class中定义的类型

  • DESCRIPTION:类型描述,Obejct header 表示对象头,alignment表示对齐填充

  • VALUE:对应内存中存储的值

锁信息

参考《深入理解 Java 虚拟机》,各种锁在 MarkWord 中锁标识位的使用情况如下图所示:

对象Mark Word字节说明:

对照 对象内存输出结果可知,结果里表示锁信息的二进制是001, 所以是一个无锁状态。

HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁。试下如下情况:

public static void main(String[] args) throws InterruptedException {

        //线程sleep 5s,确保虚拟机中偏向锁开启
        Thread.sleep(5000);
        new Thread(() -> {
            Object o = new Object();
            synchronized (o) {
                //当前锁对象第一次被线程获取
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();


    }

偏向锁图:

在线程sleep 5s 并创建新县城去获取锁对象,因为当前只有一个线程来获取这个锁对象,虚拟机就会把标志位设置为偏向锁(101),并且将获取这个锁的线程ID放到对象的Mark Word 中,具体的字节对应可以将偏向锁图和Mark Word字节说明对应来看

模拟重量级锁:

    static Object o = new Object();
    public static void main(String[] args) throws InterruptedException {
        //线程sleep 5s,确保虚拟机中偏向锁开启
        Thread.sleep(5000);
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                synchronized (o) {
                    //当前锁对象第一次被线程获取
                    System.out.println(ClassLayout.parseInstance(o).toPrintable());
                }
            }).start();
        }

    }

重量级锁图:

实例数据

实例数据(Instance Data)保存的是对象真正存储的有效信息,保存了代码中定义的各种数据类型的字段内容,并且如果有继承关系存在,子类还会包含从父类继承过来的字段。

Type

Bytes

byte,boolean

1

char,short

2

int,float

4

long,double

8

public class Customer {
    private Long id;
    private Integer status;
    private Long createdAt;
    private Long updatedAt;
    private String uniqueCode;
}

注意:

属性的排列顺序与在类中定义的顺序不同,这是因为jvm会采用字段重排序技术,对原始类型进行重新排序,以达到内存对齐的目的。具体规则遵循如下:

  • 按照数据类型的长度大小,从大到小排列

  • 具有相同长度的字段,会被分配在相邻位置

  • 如果一个字段的长度是L个字节,那么这个字段的偏移量(OFFSET)需要对齐至nL(n为整数)

对齐填充

在Hotspot的自动内存管理系统中,要求对象的起始地址必须是8字节的整数倍,也就是说对象的大小必须满足8字节的整数倍。因此如果实例数据没有对齐,那么需要进行对齐补全空缺,补全的bit位仅起占位符作用,不具有特殊含义。

在开启指针压缩的情况下,如果类中有long/double类型的变量时,会在对象头和实例数据间形成间隙(gap),为了节省空间,会默认把较短长度的变量放在前边,这一功能可以通过jvm参数进行开启或关闭:


# 开启
-XX:+CompactFields
# 关闭
-XX:-CompactFields

总结

锁升级过程的主体参与在Mark Word 中,可以对其进行深入了解。理解从无锁转换为偏向锁再至轻量级锁,重量级锁的变化。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值