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