java对象头MarkWord探索

关于java对象头markword的文章有很多,基本都是说markword用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。具体是怎么存,怎么切换这些说的很少,这里使用hsdb深入跟踪了对象头markword,在这里记录下。

实验环境:

mac ox 10.12.6

jdk8 64-Bit

涉及工具:

jdb,hsdb

一. 局部变量生命周期 和 synchronized匿名锁执行过程

java对象头MarkWord探索_第1张图片
图1
java对象头MarkWord探索_第2张图片
图2

breakM()方法栈帧局部变量表元素:

1.lock对象

2.a变量

3.synchronized匿名monitor对象

4.b变量

5.synchronized匿名throwable对象

=》 第3个和第4个变量生命周期只在synchronized块中,所以出了synchronized块后,变量c放入局部变量表第3个位置,变量d放入局部变量表第4个位置。(只根据定义确认变量生命周期,不管在定义域内后面是否会使用)

说下这个主要是为了后面理解分析栈帧时,能清楚的知道栈帧局部变量表在每一行处时存储的值的意义。

二.  先找一段openjdk关于对象头markword描述的源码说明

java对象头MarkWord探索_第3张图片
图3

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分析

java对象头MarkWord探索_第4张图片
图4

break1堆栈:

java对象头MarkWord探索_第5张图片
图5

当前markword = 0x0000007fbe847c01,无锁/无偏向/hashcode,栈帧顶部无 Lock Record 记录;

break2堆栈:

java对象头MarkWord探索_第6张图片
图6

当前markword = 0x000070000cd93868,指向栈顶新加的 Lock Record 记录;

Lock Record记录:8byte 存锁对象之前的markword值,8byte 存指向锁对象的指针;

=》单线程场景下,不存在任何锁竞争/CAS失败,一切复合预期;

六. 多线程下加锁前后锁对象markword分析

java对象头MarkWord探索_第7张图片
图7

6.1 main线程到break2,new线程到break1 堆栈:

java对象头MarkWord探索_第8张图片
图8

main线程获取锁,main线程当前栈帧为获取到锁记录的状态;new线程还未进入同步块;

当前锁对象的markword指向main线程栈顶Lock Record记录;

6.2 main线程先到break2(已获取锁,在同步块中未退出),new线程后到break2(尝试获取锁):

java对象头MarkWord探索_第9张图片
图9

可以看到,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(已经获取到锁):

java对象头MarkWord探索_第10张图片
图10

main线程释放锁之后,new线程获取到锁,此时只有new线程占用锁,但是由于已经膨胀为重量级锁了,此时new线程获取的锁记录依然是这个重量级锁。

main线程释放锁后,main线程的栈顶Lock Record发生了一点变化,Lock Record中原本指向锁对象地址的指针,现在变为指向空地址的指针了。

6.4 main线程出了同步块,new线程出了同步块:

锁对象markword恢复成最后指向的Lock Record记录中前8byte中存储的锁对象进入同步块之前的markword值,由此,锁对象恢复到进入同步块之前的状态。


至此,对markword的含义有了更清晰的理解。

你可能感兴趣的:(java对象头MarkWord探索)