对象头详解

 image.png

MarkWord

在这里插入图片描述

 biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。

lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_locklock一起,表达锁状态

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中
thread持有偏向锁的线程ID

epoch:偏向锁的时间戳。

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor(互斥量,即重量级锁)的指针

锁升级

通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。但是重量级锁会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。JVM内部为提高效率,不会在一开始就使用重量级锁。而是存在锁升级机制

        1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是上图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。

        2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如上图第二种情形。

        3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。

        4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。如下图第四种情形。

问题1:为什么要进行锁升级?锁了就锁了,不就要加锁么?

首先明确早起jdk1.2效率非常低。那时候syn就是重量级锁,申请锁必须要经过操作系统老大kernel进行系统调用,入队进行排序操作,操作完之后再返回给用户态。

内核态:用户态如果要做一些比较危险的操作直接访问硬件,很容易把硬件搞死(格式化,访问网卡,访问内存干掉、)操作系统为了系统安全分成两层,用户态和内核态 。申请锁资源的时候用户态要向操作系统老大内核态申请。Jdk1.2的时候用户需要跟内核态申请锁,然后内核态还会给用户态。这个过程是非常消耗时间的,导致早期效率特别低。有些jvm就可以处理的为什么还交给操作系统做去呢?能不能把jvm就可以完成的锁操作拉取出来提升效率,所以也就有了锁优化。

问题2:为什么要有偏向锁?

其实这本质上归根于一个概率问题,统计表示,在我们日常用的syn锁过程中70%-80%的情况下,一般都只有一个线程去拿锁,例如我们常使用的System.out.println、StringBuffer,虽然底层加了syn锁,但是基本没有多线程竞争的情况。那么这种情况下,没有必要升级到轻量级锁级别了。偏向的意义在于:第一个线程拿到锁,将自己的线程信息标记在锁上,下次进来就不需要在拿去拿锁验证了。如果超过1个线程去抢锁,那么偏向锁就会撤销,升级为轻量级锁,其实我认为严格意义上来讲偏向锁并不算一把真正的锁,因为只有一个线程去访问共享资源的时候才会有偏向锁这个情况。
无意使用到锁的场景:


/***StringBuffer内部同步***/
public synchronized int length() {
  return count;
} 
 
//System.out.println 无意识的使用锁
public void println(String x) {
   synchronized (this) {
     print(x);
     newLine();
   }
 }

问题3:为什么jdk8要在4s后开启偏向锁?

其实这是一个妥协,明确知道在刚开始执行代码时,一定有好多线程来抢锁,如果开了偏向锁效率反而降低,所以上面程序在睡了5s之后偏向锁才开放。为什么加偏向锁效率会降低,因为中途多了几个额外的过程,上了偏向锁之后多个线程争抢共享资源的时候要进行锁升级到轻量级锁,这个过程还的把偏向锁进行撤销在进行升级,所以导致效率会降低。为什么是4s?这是一个统计的时间值。

当然我们是可以禁止偏向锁的,通过配置参数-XX:-UseBiasedLocking = false来禁用偏向锁。jdk15之后默认已经禁用了偏向锁。本文是在jdk8的环境下做的锁升级验证。

2 锁的升级流程

上面已经验证了对象从创建出来之后进内存从无锁状态->偏向锁(如果开启了)->轻量级锁的过程。对于锁升级的流程继续往下,轻量级锁之后就会变成重量级锁。首先我们先理解什么叫做轻量级锁,从一个线程抢占资源(偏向锁)到多线程抢占资源升级为轻量级锁,线程如果没那么多的话,其实这里就可以理解为CAS,也就是我们说的Compare and Swap,比较并交换值。在并发编程中最简单的一个例子就是并发包下面的原子操作类AtomicInteger。在进行类似++操作的时候,底层其实就是CAS锁。
 


public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}
 
public final int getAndAddInt(Object var1, long var2, int var4) {
   int var5;
   do {
       var5 = this.getIntVolatile(var1, var2);
   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 
   return var5;
}

问题4:什么情况下轻量级锁要升级为重量级锁呢?

首先我们可以思考的是多个线程的时候先开启轻量级锁,如果它carry不了的情况下才会升级为重量级。那么什么情况下轻量级锁会carry不住。1、如果线程数太多,比如上来就是10000个,那么这里CAS要转多久才可能交换值,同时CPU光在这10000个活着的线程中来回切换中就耗费了巨大的资源,这种情况下自然就升级为重量级锁,直接叫给操作系统入队管理,那么就算10000个线程那也是处理休眠的情况等待排队唤醒。2、CAS如果自旋10次依然没有获取到锁,那么也会升级为重量级。

总的来说2种情况会从轻量级升级为重量级,10次自旋或等待cpu调度的线程数超过cpu核数的一半,自动升级为重量级锁。看服务器CPU的核数怎么看,输入top指令,然后按1就可以看到。
————————————————
版权声明:本文为CSDN博主「阿里云云栖号」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yunqiinsight/article/details/118416218

问题5:都说syn为重量级锁,那么到底重在哪里?

JVM偷懒把任何跟线程有关的操作全部交给操作系统去做,例如调度锁的同步直接交给操作系统去执行,而在操作系统中要执行先要入队,另外操作系统启动一个线程时需要消耗很多资源,消耗资源比较重,重就重在这里。

Klass Word(类指针)

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

每个Class的属性指针(即静态变量)
每个对象的属性指针(即对象变量)
普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

数组长度

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

转载自:https://blog.csdn.net/scdn_cp/article/details/86491792