关于java对象头markword的文章有很多,基本都是说markword用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。具体是怎么存,怎么切换这些说的很少,这里使用hsdb深入跟踪了对象头markword,在这里记录下。
实验环境:
mac ox 10.12.6
jdk8 64-Bit
涉及工具:
jdb,hsdb
一. 局部变量生命周期 和 synchronized匿名锁执行过程
breakM()方法栈帧局部变量表元素:
1.lock对象
2.a变量
3.synchronized匿名monitor对象
4.b变量
5.synchronized匿名throwable对象
=》 第3个和第4个变量生命周期只在synchronized块中,所以出了synchronized块后,变量c放入局部变量表第3个位置,变量d放入局部变量表第4个位置。(只根据定义确认变量生命周期,不管在定义域内后面是否会使用)
说下这个主要是为了后面理解分析栈帧时,能清楚的知道栈帧局部变量表在每一行处时存储的值的意义。
二. 先找一段openjdk关于对象头markword描述的源码说明
index[0,1]:锁标识/模式标识,一方面确定是否有锁,一方面确认字段解析方式
1.index[0,1] = 01 无锁
[header | 0 | 01] unlocked regular object header
此时需要判断 index[2],是否有偏向标识
1.1 index[2] = 0 无偏向,此时字段结构为:
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
1.2 index[2] = 1 偏向,此时字段结构为:
JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
==》
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
2.index[0,1] = 00 轻量级锁
[ptr | 00] locked ptr points to real header on stack
3.index[0,1] = 10 重量级锁
[ptr | 10] monitor inflated lock (header is wapped out)
4.index[0,1] = 11 GC markSweep标志,标记对象不可用
[ptr | 11] marked used by markSweep to mark an object not valid at any other time
三. 先看个简单的demo
public class TestHashCode{
public static void main(String[] args){
breakM();
}
public static void breakM(){
LockBean lock = new LockBean(); //_mark = 0x0000000000000005 0101 无锁,任意偏向
int a = lock.hashCode(); //_mark = 0x0000007fbe847c01 0001 无锁,无偏向,hashcode
synchronized(lock){ //_mark = 0x0000700004b13870 0000 轻量级锁,栈顶指针
int b = lock.hashCode(); //_mark = 0x0000700004b13870 0000
}
int c = lock.hashCode(); //_mark = 0x0000007fbe847c01
int d = 32;
}
}
breakM方法里先定义了一个对象,再取该对象hashcode值,然后对该对象加锁。后面_mark表示执行完这一行后该对象的markword值,是很清晰的一段markword变更流程,我们下面做具体分析。
四. 轻量级锁执行过程分析
代码进入同步块时,如果此同步对象没有被锁定(markword锁标志为01),jvm首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的MarkWord拷贝(8byte存当前markword值拷贝,8byte存当前对象地址)。
=》如果此同步对象已被锁定(markword锁标志为00/10),则进入锁等待
然后尝试使用CAS将当前锁对象的MarkWord更新为指向Lock Record的指针。如果更新成功,则这个线程就获取了该对象的锁,并且该对象MarkWord的锁标识位(最后2bit)将转变为00,即表示该对象当前处于轻量级锁状态。
若更新操作失败,jvm会先检查该对象MarkWord是否已经是指向当前线程的栈帧,若是则说明已经获取过锁了,就直接进入同步块执行。否则说明当前锁对象已经被其他线程抢占了。
如果有多个线程同时争用同一个锁,此时轻量级锁要膨胀为重量级锁,锁标志位状态值变为10,Mark Word中存储的是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。(膨胀为重量级锁后,会生成一个新的锁Lock Record记录。锁对象markword指向新生成的重量级锁Lock Record记录,并把锁标志位置为10。)
轻量级锁的解锁也是通过CAS来做的,将Lock Record存储的之前markword值,CAS更新回锁对象的MarkWord中,更新成功则整个同步块完成;更新失败,则说明有其他线程尝试过获取该锁,那就要在释放锁的同时唤醒被挂起的线程。
=》多线程争用锁时,锁对象markword会变更为指向 新的重量级锁Lock Record地址;=》锁对象markword指向的重量级锁Lock Record地址 和 当前线程栈顶Lock Record地址不一致,说明锁膨胀为重量级锁了,存在多线程竞争。则当前线程释放锁,同时唤醒被挂起的线程。
五. 单线程下加锁前后锁对象markword分析
break1堆栈:
当前markword = 0x0000007fbe847c01,无锁/无偏向/hashcode,栈帧顶部无 Lock Record 记录;
break2堆栈:
当前markword = 0x000070000cd93868,指向栈顶新加的 Lock Record 记录;
Lock Record记录:8byte 存锁对象之前的markword值,8byte 存指向锁对象的指针;
=》单线程场景下,不存在任何锁竞争/CAS失败,一切复合预期;
六. 多线程下加锁前后锁对象markword分析
6.1 main线程到break2,new线程到break1 堆栈:
main线程获取锁,main线程当前栈帧为获取到锁记录的状态;new线程还未进入同步块;
当前锁对象的markword指向main线程栈顶Lock Record记录;
6.2 main线程先到break2(已获取锁,在同步块中未退出),new线程后到break2(尝试获取锁):
可以看到,new线程进入同步块代码后,new线程的栈帧顶部也添加了 Lock Record 记录。
Q:=》new线程的 Lock Record前8byte存的值是 0x00..03,这个值是怎么来的?
A:=》最后2bit 11 在markword定义里表示对象处于GC mark不可用状态。在发生锁争用时锁膨胀为重量级锁,线程进入锁等待状态后,线程栈顶的Lock Record其实存在已经没有意义了。
main线程栈帧保持不变;
锁对象的markword值更新为 0x00007fcb19846dfa,既不指向main线程Lock Record,也不指向new线程Lock Record。
此时markword锁标志位为 10,意味着由于多线程获取锁,锁升级为 重量级锁 了,其他位的值,是指向重量级锁的指针值。
取其他位的值,去掉锁标志位的值并用00填充,得到重量级锁指针 0x00007fcb19846df8,查看该重量级锁记录,可以看到,也是存的一个Lock Record记录,前8byte存的锁对象之前无锁时的markword值,后8byte存的指向锁对象的指针;
Q:=》重量级锁Lock Record中前8byte存放的锁对象markword值,这个值是怎么取到或计算的?
A: 取的当前锁对象所指向的Lock Record记录中存放的markword值。
轻量级锁时,在同步块中运行对象hashcode方法后,由于要改变锁对象markword值(存储hashcode值),jvm的做法是:生成一个新的Lock Record记录(栈帧外),该新的Lock Record记录中存放锁对象含hashcode的markword值,然后将锁对象markwrd指向新的Lock Record记录;退出同步块时,也是从这里恢复锁对象markword值;
若已经是重量级锁,则运行锁对象hashcode方法后,将更新重量级锁Lock Record记录中的markword值为含hashcode的markword值。
6.3 main线程出了同步块,new线程在break2(已经获取到锁):
main线程释放锁之后,new线程获取到锁,此时只有new线程占用锁,但是由于已经膨胀为重量级锁了,此时new线程获取的锁记录依然是这个重量级锁。
main线程释放锁后,main线程的栈顶Lock Record发生了一点变化,Lock Record中原本指向锁对象地址的指针,现在变为指向空地址的指针了。
6.4 main线程出了同步块,new线程出了同步块:
锁对象markword恢复成最后指向的Lock Record记录中前8byte中存储的锁对象进入同步块之前的markword值,由此,锁对象恢复到进入同步块之前的状态。
至此,对markword的含义有了更清晰的理解。