java对象头的Mark Word

前言

最近在做excel解析的编码,其中涉及到一个内存占用空间优化的问题。解决的方法是尽量少的创建对象,可以共用的对象信息不用创建多份。查阅资料后得到如下文章,作为学习记录使用。
JAVA对象头

由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能 。在学习并发编程知识synchronized时,我们总是难以理解其实现原理,因为偏向锁、轻量级锁、重量级锁都涉及到对象头,所以了解java对象头是我们深入了解synchronized的前提条件,以下我们使用64位JDK示例

    获取一个对象布局实例

    首先在maven项目中 引入查看对象布局的jar包

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

    1
    2
    3
    4
    5

    调用如下方法获取布局
    ClassLayout.parseInstance(instance).toPrintable()

// 输出 instance对象 的布局
 System.out.println(ClassLayout.parseInstance(instance).toPrintable());

    1
    2

    得到如下结果
    在这里插入图片描述
    各个参数解释如下:
    OFFSET:偏移地址,单位字节;
    SIZE:占用的内存大小,单位为字节;
    TYPE DESCRIPTION:类型描述,其中object header为对象头;
    VALUE:对应内存中当前存储的值;
    通过偏移地址可以知道,对象头占用了12个字节,12*8bit=96bit。

    指针压缩

    一般在jdk8之前的话得到的结果是16*8=128bit的,但是jdk8之后,由于默认开启了指针压缩所有会得到96bit的结果。
    如果要测试的话,可以通过关闭指针压缩的参数设置,如下所示。

-XX:-UseCompressedOops

    1

在这里插入图片描述
8. 关闭之后再次运行得到如下所示:
在这里插入图片描述
可以看到偏移地址变成了16位了。所以可以看到:开启指针压缩可以减少对象的内存使用。因此,开启指针压缩,理论上来讲,大约能节省三分之一的对象头内存。jdk8及以后版本已经默认开启指针压缩,无需配置。

    普通对象压缩后获取结构:
    在这里插入图片描述
    普通的对象获取到的对象头结构为:
    在这里插入图片描述
    数组对象获取到的对象头结构为:
    在这里插入图片描述
    在这里插入图片描述
    对象头的组成
    我们先了解一下,一个JAVA对象的存储结构。在Hotspot虚拟机中,对象在内存中的存储布局分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
    在我们刚刚打印的结果中可以这样归类:

OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)    //markword             01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)    //markword             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)   //klass pointer 类元数据 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true    // Instance Data 对象实际的数据
     13     3           (loss due to the next object alignment)            //Padding 对齐填充数据

    1
    2
    3
    4
    5
    6

下面让我们看一下对象里面的东西:
Mark Word

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

    32位
    在这里插入图片描述
    64位
    在这里插入图片描述
    其中各部分的含义如下:
    lock
    其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
    JVM一般是这样使用锁和Mark Word的转载自:

    当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
    当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
    当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
    当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
    偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
    轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
    自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
————————————————
版权声明:本文为CSDN博主「daimeijin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/daimeijin/article/details/119257639

posted @ 2022-04-12 14:56  甜菜波波  阅读(501)  评论(0编辑  收藏  举报