对象头
JAVA对象头在hotspot中包括两部分数据:Mark Word(标记字段) 和Klass Pointer(类型指针)
数组对象头中还存在一个数组长度(array length)
Mark Word
默认存储对象的hashcode,分代年龄,偏向锁标识和锁标志位。这些信息都是和对象自身定义无关的数据,所以mark word被设计成了一个非固定结构以便在极小的空间中存储尽量多的信息。它会根据对象自身的状态复用自己的存储空间,也就是说在运行期间mark word里存储的数据会随着锁标志位的变化而变化。
Klass Pointer
对象指向对类元数据的指针,虚拟机根据这个指针来确定这个对象是哪个类的实例。
32位和64位不同
32位虚拟机的mark word和klass pointer分别占用32bits的字节
64位虚拟机的mark word和klass pointer分别占用64bits的字节
Mark Word分配情况
32位
64位
关于锁的降级
锁降级是会在线程不占用锁的情况下进行降级,无法降级只是在占用的情况下无法降级
如果可偏向锁升级过,当无线程占用的时候只会降级成无锁不可偏向状态,不能再次偏向了(不考虑重偏向和偏向撤销,因为重偏向只有在通过一个类创建了大量对象当做锁,并且有两个及以上线程使用的时候才会出现)。
identity hashcode和偏向锁
如果一个对象已经生成了identity hashcode,那么就不能再使用偏向锁。
偏向锁
当jvm开启了偏向锁(JDK6以上默认开启),新创建的对象的Mark Word中的线程ID为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。
-XX:-UseBiasedLocking // 关闭偏向锁
-XX:BiasedLockingStartupDelay=0 // 关闭偏向锁延时(默认开启偏向锁,启动后第5秒才能生效)
偏向锁获取过程:
- 判断Mark Word中的锁标志位是否为01,并且偏向锁标志位1,如果是说明可偏向;
- 判断线程ID是否指向当前线程,如果是,不需要加锁,直接在当前线程栈中添加一个空的lock record,用来记录重入次数,然后执行同步代码;
- 如果线程ID没有指向当前线程,则通过cas竞争锁,如果竞争成功(匿名偏向状态会成功),则将Mark Word中的线程ID设置为当前线程,然后在当前线程栈中添加一个空的lock record,然后执行同步代码;
- 如果竞争失败,说明已有线程使用过该偏向锁。这时当到达全局安全点时,获得偏向锁的线程会被挂起,然后升级为轻量级锁(具体开升级轻量级锁,因为持有偏向锁的线程可能存在也可能已销毁),然后被阻塞在安全点的线程继续往下执行同步线程;
偏向锁的释放:
持有偏向锁的线程执行完同步代码块的时候,只会删除线程栈中的lock record,并不会主动修改Mark Word中的线程ID,也就是偏向锁不会自动释放, 只有遇到其它线程竞争的时候才会释放偏向锁。
轻量级锁
加锁过程:
- 首先判断Mark Word中的锁状态是无锁状态(锁标识位为01,偏向锁标识为0);
- 虚拟机会现在线程栈中建立一个名为锁记录(lock record)的空间,用于存储当前锁对象目前的Mark Word的拷贝(Displaced Mark Word),然后将锁对象中的Mark Word拷贝到锁记录中;
- 然后虚拟机使用cas尝试将锁对象的Mark Word更新为指向锁记录的指针,并将锁记录中的owner指针指向锁对象的Mark Word。如果更新成功,说明这个线程成功获取了该对象的锁,此时将锁对象的锁标志位改为00;
- 如果更新失败了,虚拟机会检查锁对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了该对象的锁,直接执行同步代码即可;
- 如果不是指向当前线程,说明其它线程竞争,当前线程使用自旋来获取锁,如果有两个以上的线程竞争同一个锁,那轻量级锁直接膨胀为重量级锁,锁标志变为10,Mark Word中存储的就是指定重量级(互斥量)的指针;
轻量级锁的释放
- 使用cas操作将Displaced Mark Word替换回对象头,如果成功,则表明没有竞争发生,释放锁成功;
- 如果失败,说明锁已经升级为了重量级锁,Mark Word中的指针也已经指向了Monitor object,竞争锁的线程已经阻塞了,此时需要释放锁,并唤醒阻塞的线程重新竞争锁;
重量级锁
重量级锁Mark Word中指针指向的是Monitor对象(也称管程或监视器锁)的起始地址。每个对象都存在着一个monitor对象与之关联。 对象与其monitor之间的关系存在多种实现方式:1.与对象一起创建或销毁;2:当线程获取对象锁时自动生成。 当一个monitor被某个线程持有后,它便处于锁定状态。
加锁过程:
- 当多个线程同时访问同步代码时,首先会进入Entry Set,如果获取到对象的monitor,会进入The owner区域,并把monitor中的owner设置为当前线程,同时monitor中的计数器+1;
- 如果获取失败,则进入Wait set,线程阻塞;
重量级锁的释放
- 如果执行完同步代码,将释放当前持有的monitor,owner变为null,count-1,并唤醒在Wait Set中的线程重新竞争锁;
- 如果没有执行完同步代码,而是调用了wait()方法,除了第一步的处理外,还需要将当前线程阻塞并放入Wait Set中;
示意图
epoch的作用
epoch在对象头的mark word中存在,在对象所属的类实例(instanceClass)中也存在一个epoch的值。 此外还有一个time阈值(默认25s)用来重置epoch值,如果自从上次执行批量重偏向已经超过了这个阈值时间,就会发生epoch 重置。
什么时候会发生偏向撤销?
偏向撤销的计数是针对类的,不是针对类对象的。
线程B在获取锁的时候,如果发现偏向锁的线程ID已是其它线程,这时候会触发偏向撤销,升级为轻量级锁(偏向锁撤销次数<=20有效,如果20<撤销次数<=40,则会进行重偏向),如果偏向撤销达到40的阈值,就会将该类标记为不可偏向。
偏向撤销每一个实例只会出现一次偏向撤销,因为对于一个实例来说,如果偏向后还有其它线程来获取锁,会直接升级为轻量级锁,这时候会发生偏向撤销,但是,这时线程释放锁后,对象锁变为了无锁(不可偏向)的状态,再有其它线程获取锁的时候,直接就是轻量级锁了,因此也就不会出现偏向撤销了。
-XX:BiasedLockingBulkRevokeThreshold = 40
具体过程(升级轻量级锁) :
- 偏向锁的撤销需要等待安全点(safe point,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程;
- 检查持有偏向锁的线程状态(会遍历当前jvm中所有的线程,查看该线程是否还存在),如果线程还存在,则检查线程是否在执行同步代码块中的代码(通过判断是否有lock record),如果是,则升级为轻量级锁(会先变为无锁状态再升级为轻量级锁)。
- 如果持有偏向锁的线程不存在或者未在执行同步代码块中的代码,则进行判断是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态,然后升级为轻量级锁。
什么时候会发生重偏向?
重偏向的计数也是针对类的,不是针对类对象的。
默认情况下,当该对象锁所属类已经进行了20次偏向撤销后,就允许进行重偏向。
每次发生重偏向时,不仅会将类实例的epoch的值+1,而且会遍历JVM中所有的线程栈,找到该class正处于加锁状态的偏向锁对象,然后将其epoch字段改为新值。下次获取锁时,发现当前对象的epoch值和class的epoch值不相等,那么就算当前已经偏向了其它线程,也 不会执行撤销操作,而是直接通过cas将Mark Word的线程ID改为当前线程ID。
-XX:BiasedLockingBulkRebiasThreshold = 20
上面为什么不会执行撤销操作了?
因为如果epoch的值不一致,那么此时肯定是处于重偏向的状态,也就是可以重新偏向了,而不用升级为轻量级锁了,所以也就不用执行撤销了。