Java对象头Mark Word结构解析:锁状态、GC标记与HashCode存储细节

Java 对象头 Mark Word 结构解析:锁状态、GC 标记与 HashCode 存储细节

大家好,今天我们深入探讨 Java 对象头的 Mark Word 结构,这是理解 JVM 内存布局和锁机制的关键。Mark Word 存储了对象的哈希码、GC 分代年龄、锁状态等重要信息。理解 Mark Word 的结构和变化对于排查并发问题、优化 GC 策略至关重要。

1. 对象头概述

在 HotSpot 虚拟机中,Java 对象在内存中由三个部分组成:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其中,对象头又包含两部分:

  • Mark Word (标记字):存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志等。
  • Klass Pointer (类型指针):指向对象所属类的元数据指针。通过这个指针,JVM 可以确定对象的类型。如果对象是一个数组,对象头还会包含数组的长度。

我们今天主要聚焦 Mark Word。

2. Mark Word 的结构

Mark Word 的长度在 32 位 JVM 中是 4 字节,在 64 位 JVM 中是 8 字节。但要注意,64 位 JVM 开启压缩指针后,类型指针会压缩到 4 字节,而Mark Word仍然是8字节。Mark Word 的结构并不是固定的,它会随着对象的状态变化而改变,存储不同的信息。

以下是 HotSpot 虚拟机中 Mark Word 的几种常见结构:

2.1. 无锁状态 (Normal)

Offset (bits) Length (bits) 内容
0 31/63 hashCode
31/63 1 biased_lock (是否启用偏向锁,1表示启用)
32/64 2 age (GC 分代年龄)
34/66 1 lock (锁标志位,01 表示无锁状态)
  • hashCode: 对象的哈希码,当对象调用 hashCode() 方法时才会计算并存储。
  • biased_lock: 偏向锁标志位,表示是否启用偏向锁。
  • age: GC 分代年龄,用于判断对象是否应该被回收。
  • lock: 锁标志位,用于标识对象的锁状态。

2.2. 偏向锁状态 (Biased)

Offset (bits) Length (bits) 内容
0 54/62 thread (持有偏向锁的线程 ID)
54/62 1 epoch (偏向时间戳)
55/63 1 age (GC 分代年龄)
56/64 1 biased_lock (是否启用偏向锁,1表示启用)
57/65 1 lock (锁标志位,01 表示偏向锁状态)
  • thread: 持有偏向锁的线程 ID。
  • epoch: 偏向时间戳,用于判断偏向锁是否过期。
  • age: GC 分代年龄。
  • biased_lock: 偏向锁标志位。
  • lock: 锁标志位。

2.3. 轻量级锁状态 (Lightweight Locked)

Offset (bits) Length (bits) 内容
0 62/30 ptr_to_lock_record (指向锁记录的指针)
62/30 2 lock (锁标志位,00 表示轻量级锁状态)
  • ptr_to_lock_record: 指向锁记录的指针。锁记录存储在线程的栈帧中。
  • lock: 锁标志位。

2.4. 重量级锁状态 (Heavyweight Locked)

Offset (bits) Length (bits) 内容
0 62/30 ptr_to_monitor (指向 Monitor 对象的指针)
62/30 2 lock (锁标志位,10 表示重量级锁状态)
  • ptr_to_monitor: 指向 Monitor 对象的指针。Monitor 对象是操作系统级别的锁。
  • lock: 锁标志位。

2.5. GC 标记状态 (Marked)

Offset (bits) Length (bits) 内容
0 62/30 mark_word (GC 标记)
62/30 2 lock (锁标志位,11 表示 GC 标记状态)
  • mark_word: GC 标记,用于在垃圾回收过程中标记对象。
  • lock: 锁标志位。

3. 锁状态转换

Java 中的锁机制是一个逐步升级的过程,目的是在不同的并发场景下提供最佳的性能。锁状态转换的顺序通常是:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。 这种锁升级的过程避免了在不必要的场景下使用重量级锁,从而提高了并发性能。

  • 偏向锁: 当一个线程访问同步块并获取锁时,会在对象头的 Mark Word 中记录该线程的 ID。以后该线程再次进入同步块时,不需要进行任何同步操作,直接可以获取锁。 偏向锁适用于只有一个线程访问同步块的场景。
  • 轻量级锁: 当多个线程竞争同一个锁时,偏向锁会升级为轻量级锁。线程会在自己的栈帧中创建一个锁记录(Lock Record),并将对象头的 Mark Word 复制到锁记录中。 然后,线程尝试使用 CAS 操作将对象头的 Mark Word 更新为指向锁记录的指针。 如果更新成功,则线程获得锁;如果更新失败,则表示存在竞争,锁会膨胀为重量级锁。
  • 重量级锁: 当多个线程竞争轻量级锁失败时,锁会膨胀为重量级锁。 重量级锁使用操作系统底层的互斥量(Mutex)来实现,线程需要进入阻塞状态才能获取锁。

4. 源码分析 (基于 OpenJDK)

理解 Mark Word 的结构,最好的方式是阅读 HotSpot 虚拟机的源码。 由于 HotSpot 虚拟机源码复杂,我们这里只给出一些关键的入口点和相关代码片段,帮助大家理解其实现原理。

  • markOop.hpp: 定义了 markOop 类,这是 Mark Word 的抽象表示。
  • oop.hpp: 定义了 oopDesc 类,这是 Java 对象的抽象基类。oopDesc 包含了 markOop 类型的成员变量,表示对象的 Mark Word。
  • synchronizer.cpp: 包含了锁相关的实现,例如偏向锁、轻量级锁和重量级锁的获取和释放。
  • gcOopRecorder.cpp: 包含了 GC 相关的实现,例如对象的标记和扫描。

以下是一些关键的代码片段(简化版):

// markOop.hpp
class markOopDesc : public oopDesc {
  friend class VMStructs;
 private:
  //  bits 0..2 used as lock bits
  //      00 - unlocked
  //      01 - monitor (normal lock)
  //      10 - biased lock
  //      11 - marked for gc
  //
  //  bits 3..30 used to store hash value
  //  bits 31 used to indicate age
  //  32 bit word
  jint _mark;

 public:
  // ...
  inline bool is_unlocked() const { return (_mark & markWord::lock_mask) == markWord::unlocked_value; }
  inline bool is_biased_locked() const { return (_mark & markWord::lock_mask) == markWord::biased_lock_value; }
  inline bool is_lightweight_locked() const { return (_mark & markWord::lock_mask) == markWord::lightweight_lock_value; }
  inline bool is_heavyweight_locked() const { return (_mark & markWord::lock_mask) == markWord::heavyweight_lock_value; }
  inline bool has_identity_hash() const { return !is_unlocked() && !is_biased_locked() && !is_marked(); }

  // ...
};

这段代码展示了 markOopDesc 类的定义,以及如何通过位运算来判断锁的状态。markWord::lock_mask 定义了锁标志位的掩码,通过与 _mark 进行与运算,可以提取出锁标志位的值,从而判断锁的状态。

// synchronizer.cpp
// Attempt to acquire biased lock.
static inline markOop attempt_biased_locking(oop obj, Thread *thread) {
  markOop mark = obj->mark();
  if (mark == NULL) {
    return NULL; // Object is not eligible for biased locking
  }

  if (mark->has_bias_pattern()) {
    // Already biased towards another thread.
    if (mark->biased_locker() == thread) {
      // Re-bias towards this thread.
      return mark;
    } else {
      // Revoke the bias.
      return NULL;
    }
  } else {
    // Attempt to bias the object towards this thread.
    markOop new_mark = mark->set_biased_locker(thread);
    if (obj->cas_set_mark(mark, new_mark)) {
      return new_mark;
    } else {
      return NULL;
    }
  }
}

这段代码展示了尝试获取偏向锁的过程。首先判断对象是否已经偏向锁,如果已经偏向锁,则判断是否偏向当前线程。如果未偏向锁,则尝试使用 CAS 操作将对象头的 Mark Word 更新为偏向当前线程的 Mark Word。

5. 代码示例

为了更直观地理解 Mark Word 的变化,我们可以使用 JOL (Java Object Layout) 工具来查看对象的内存布局。

import org.openjdk.jol.info.ClassLayout;

public class MarkWordExample {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        System.out.println("无锁状态:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        synchronized (obj) {
            System.out.println("重量级锁状态:");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }

        System.out.println("退出同步块后:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

这段代码创建了一个 Object 对象,然后分别在无锁状态、重量级锁状态和退出同步块后打印对象的内存布局。需要引入jol-core依赖。

执行结果(示例,具体数值会因 JVM 版本和环境而异):

无锁状态:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)           e5 b7 00 20 (11100101 10110111 00000000 00100000) (536941541)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

重量级锁状态:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)           30 01 00 20 (00110000 00000001 00000000 00100000) (536871152)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

退出同步块后:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)           e5 b7 00 20 (11100101 10110111 00000000 00100000) (536941541)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

通过观察输出结果,我们可以看到 Mark Word 在不同锁状态下的变化。

6. HashCode 的存储

当一个对象没有被同步过,且调用了 hashCode() 方法时,JVM 会计算对象的哈希码,并将哈希码存储在 Mark Word 中。

public class HashCodeExample {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println("HashCode 调用前:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        int hashCode = obj.hashCode();
        System.out.println("HashCode: " + hashCode);

        System.out.println("HashCode 调用后:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

执行结果(示例):

HashCode 调用前:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)           d5 c5 00 20 (11010101 11000101 00000000 00100000) (536945109)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
HashCode: 1163157884
HashCode 调用后:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)           d5 c5 00 20 (11010101 11000101 00000000 00100000) (536945109)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,调用 hashCode() 方法后,对象的 Mark Word 中存储了计算得到的哈希码。

7. GC 分代年龄

GC 分代年龄用于判断对象是否应该被垃圾回收。每当对象在 Minor GC 中存活下来,其分代年龄就会加 1。当分代年龄达到某个阈值(通常是 15),对象就会被晋升到老年代。

可以通过-XX:MaxTenuringThreshold参数来设置这个阈值。

8. Mark Word 在实际应用中的作用

理解 Mark Word 的结构和变化,可以帮助我们:

  • 排查并发问题: 通过分析 Mark Word,可以确定对象的锁状态,从而诊断死锁、活锁等并发问题。
  • 优化 GC 策略: 通过分析对象的 GC 分代年龄,可以调整 GC 参数,提高垃圾回收效率。
  • 理解锁的实现原理: 深入理解 Java 锁的实现机制,更好地使用锁来解决并发问题。
  • 性能调优: 更好地理解对象在 JVM 中的存储方式,从而进行更有效的性能调优。

9. 注意事项

  • Mark Word 的结构和实现细节可能因 JVM 版本而异。
  • JOL 工具只能查看内存布局,不能直接修改 Mark Word 的值。修改 Mark Word 的值需要使用 Unsafe 类,但非常不推荐,因为它会破坏 JVM 的安全性和稳定性。
  • 锁升级是一个动态的过程,JVM 会根据实际的并发情况自动调整锁的状态。

锁状态的转换与性能

Mark Word 存储锁信息,JVM 通过 CAS 操作修改 Mark Word 实现锁的转换,从而优化并发性能。理解这些转换有助于我们编写更高效的并发程序。

HashCode 与 GC 的关系

HashCode 的计算结果会存储在 Mark Word 中,这可能会影响对象的 GC 行为。 理解 HashCode 的存储方式有助于我们更好地理解 GC 的工作原理。

从源码到实践:理解 Mark Word 的价值

通过分析源码和实际的例子,我们更深入地理解了 Mark Word 的结构和作用。 理解 Mark Word 对于排查并发问题、优化 GC 策略和进行性能调优至关重要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注