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

  1. class Calculate {
  2. private final Calculate lock = this;
  3. private int num = 10;
  4. public synchronized void add(int plusNum) {
  5. try {
  6. Thread.sleep(5 * 1000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. num += plusNum;
  11. }
  12. public void multi(int minNum) {
  13. synchronized (lock) {
  14. num *= minNum;
  15. }
  16. }
  17. public int getNum() {
  18. return num;
  19. }
  20. }
  21. public class SynchronizedMain {
  22. static ExecutorService executor = Executors.newFixedThreadPool(2);
  23. public static void main(String[] args) {
  24. final Calculate calculate = new Calculate();
  25. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
  26. }
  27. }

26 行的输出如下图,可以看到 object header 是一共占了 128bit 的
image.png

Mark Word

Mark Word 在大致分为三种状态 无锁、偏向锁、轻量级锁&重量级锁、空。查看 jdk8 markOop.hpp 源码的注释,可以看到关于 MarkWord 的解释

  1. // jdk 8 markOop.hpp 部分注释
  2. // 64 bits:
  3. // --------
  4. // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
  5. // 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类 在前面展示过)

  1. final Calculate calculate = new Calculate();
  2. System.out.println(calculate.hashCode());
  3. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());

输出的结果如下
image.png
把前 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 的输出来验证下:

  1. Thread.sleep(5000);
  2. final Calculate calculate = new Calculate();
  3. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
  4. synchronized (calculate){
  5. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
  6. }

上述代码中,停顿 5s 是为了等待偏向锁的开启,那么输出入下:
image.png
可以看到,在进入 synchronized 块以前,calculate 实例的对象头处于匿名偏向锁状态,而进入了同步模块后则升级为偏向锁

轻量级锁状态

首先看下轻量级锁的定义
语雀内容

Mark Word 64bit 状态
栈中锁记录的指针:62 锁标记:2 (00) 轻量级锁

还是测试代码加分析

  1. Thread.sleep(5000);
  2. final Calculate calculate = new Calculate();
  3. synchronized (calculate){
  4. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
  5. }
  6. new Thread(() -> {
  7. synchronized (calculate){
  8. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
  9. }
  10. }).start();

jol 输出的结果如下
image.png
第一个输出是偏向锁,偏向的是 main 线程
在第二个线程里,又访问了锁对象,所以对象就升级为了轻量级锁

重量级锁状态

同样的去看下重量锁的定义
语雀内容

**

Mark Word 64bit 状态
重量级锁的指针:62 锁标记:2 (10) 重量级锁

依然是代码和分析

  1. Thread.sleep(5000);
  2. final Calculate calculate = new Calculate();
  3. synchronized (calculate) {
  4. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
  5. }
  6. Thread thread1 = new Thread(() -> {
  7. synchronized (calculate) {
  8. System.out.println("当前线程为: "+Thread.currentThread().getName());
  9. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
  10. }
  11. });
  12. thread1.setName("thread1");
  13. Thread thread2 = new Thread(() -> {
  14. try {
  15. Thread.sleep(1);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. synchronized (calculate) {
  20. System.out.println("当前线程为: "+Thread.currentThread().getName());
  21. System.out.println(ClassLayout.parseInstance(calculate).toPrintable());
  22. }
  23. });
  24. thread2.setName("thread2");
  25. thread1.start();
  26. thread2.start();

这个我在我的电脑上不是每次都是必现的,多运行几次,就会出现轻量级锁升级为重量级锁的情况,如下
image.png
上边的输出就验证了锁升级过程,偏向->轻量级->重量级