Object header 中的 Mark Word 与 klass pointer
Object header 俗称对象头,这个直译并不直观,我的理解是它是一个对象的描述加索引,其中包含了如下信息
- Mark Word
- identityhashcode 未被覆写过的hashcode _
- age 分代年龄
- biasedlock 偏向模式_
- lock 标记位
- 00~10 锁标记位
- 11 GC 标记
- klass pointer 元数据信息指针
元信息通常是与方法区关联,通常用来保存装载的类信息,常量,静态变量,即时编译器编译后的代码等数据_
在 64 位的 JVM 环境里对象头占 128bit,其中 MarkWord和 klass pointer 各占 64 bit
这个可以使用 jol (implementation "org.openjdk.jol:jol-core:0.14"
)验证下,例如我有如下类(先要关闭指针压缩:-XX:-UseCompressedOops
)
class Calculate {
private final Calculate lock = this;
private int num = 10;
public synchronized void add(int plusNum) {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num += plusNum;
}
public void multi(int minNum) {
synchronized (lock) {
num *= minNum;
}
}
public int getNum() {
return num;
}
}
public class SynchronizedMain {
static ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
final Calculate calculate = new Calculate();
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
}
}
26 行的输出如下图,可以看到 object header 是一共占了 128bit 的
Mark Word
Mark Word 在大致分为三种状态 无锁、偏向锁、轻量级锁&重量级锁、空。查看 jdk8 markOop.hpp 源码的注释,可以看到关于 MarkWord 的解释
// jdk 8 markOop.hpp 部分注释
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
PS:说是在 jdk15 markWord.hpp 中,已经把偏向锁的相关代码删除了,但是我看注释还是有的,而且鉴于现在大多数都是 jdk8 的情况,所以还是会把偏向锁讲一下
下面的表格列举了 MarkWord 在不同状态时的占位情况,其中轻量级锁和重量级锁侵占了很多字段,这个状态可以通过 jdk8 markOop.hpp 源码注释查看到缘由
Mark Word 64bit | 状态 | |||||
---|---|---|---|---|---|---|
空:25 | hash:31 | 空:1 | GC 年龄:4 | 是否偏向:1 | 锁标记:2 (01) | 无锁 |
线程 ID:54 | epoch:2 | 空:1 | GC 年龄:4 | 是否偏向:1 | 锁标记:2 (01) | 偏向锁 |
栈中锁记录的指针:62 | 锁标记:2 (00) | 轻量级锁 | ||||
重量级锁的指针:62 | 锁标记:2 (10) | 重量级锁 | ||||
空:62 | 锁标记:2 (11) | GC 标记 |
下面就按个的去了解和验证一下
无锁状态
通过无锁状态,我可以学习一下 jol 的使用以及对 Object header 的分析
Mark Word 64bit | 状态 | |||||
---|---|---|---|---|---|---|
空:25 | hash:31 | 空:1 | GC 年龄:4 | 是否偏向:1 | 锁标记:2 (01) | 无锁 |
首先将如下代码运行下,看下输出(Calculate类 在前面展示过)
final Calculate calculate = new Calculate();
System.out.println(calculate.hashCode());
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
输出的结果如下
把前 object header 中的前 64 位也就是 MarkWord 删选出来如下
00000001 00100010 11101001 01010010 01111000 00000000 00000000 00000000
由于是小端排序,所以要将二进制反过来看
颜色指示:空、hash 值、GC 年龄、是否偏向、锁标记
小端排序: 00000001 00100010 11101001 01010010 01111000 00000000 00000000 00000000
大端排序: 00000000 00000000 00000000 01111000 01010010 11101001 00100010 00000001
可以看到紫色的 hash 值计算出来跟打印的是一致的,锁标记和偏向也都是正确的
偏向锁状态&匿名偏向锁状态
如果不了解偏向锁,可以看一下这篇博文
语雀内容
Mark Word 64bit | 状态 | |||||
---|---|---|---|---|---|---|
线程 ID:54 | epoch:2 | 空:1 | GC 年龄:4 | 是否偏向:1 | 锁标记:2 (01) | 偏向锁 |
下面还是通过代码和 jol 的输出来验证下:
Thread.sleep(5000);
final Calculate calculate = new Calculate();
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
synchronized (calculate){
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
}
上述代码中,停顿 5s 是为了等待偏向锁的开启,那么输出入下:
可以看到,在进入 synchronized 块以前,calculate 实例的对象头处于匿名偏向锁状态,而进入了同步模块后则升级为偏向锁
轻量级锁状态
首先看下轻量级锁的定义
语雀内容
Mark Word 64bit | 状态 | |||||
---|---|---|---|---|---|---|
栈中锁记录的指针:62 | 锁标记:2 (00) | 轻量级锁 |
还是测试代码加分析
Thread.sleep(5000);
final Calculate calculate = new Calculate();
synchronized (calculate){
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
}
new Thread(() -> {
synchronized (calculate){
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
}
}).start();
jol 输出的结果如下
第一个输出是偏向锁,偏向的是 main 线程
在第二个线程里,又访问了锁对象,所以对象就升级为了轻量级锁
重量级锁状态
同样的去看下重量锁的定义
语雀内容
**
Mark Word 64bit | 状态 | |||||
---|---|---|---|---|---|---|
重量级锁的指针:62 | 锁标记:2 (10) | 重量级锁 |
依然是代码和分析
Thread.sleep(5000);
final Calculate calculate = new Calculate();
synchronized (calculate) {
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
}
Thread thread1 = new Thread(() -> {
synchronized (calculate) {
System.out.println("当前线程为: "+Thread.currentThread().getName());
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
}
});
thread1.setName("thread1");
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (calculate) {
System.out.println("当前线程为: "+Thread.currentThread().getName());
System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
}
});
thread2.setName("thread2");
thread1.start();
thread2.start();
这个我在我的电脑上不是每次都是必现的,多运行几次,就会出现轻量级锁升级为重量级锁的情况,如下
上边的输出就验证了锁升级过程,偏向->轻量级->重量级