Java对象头Mark Word:如何存储分代年龄(Age)与偏向锁的线程ID

Java 对象头 Mark Word:分代年龄与偏向锁的线程 ID 深入解析

大家好,今天我们来深入探讨 Java 对象头中的 Mark Word,它在 JVM 内存管理和并发机制中扮演着至关重要的角色。我们将重点关注 Mark Word 如何存储分代年龄(Age)和偏向锁的线程 ID,并结合代码示例进行详细的讲解。

1. 对象头的构成

在 HotSpot 虚拟机中,Java 对象在内存中的布局通常包括三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头(Header): 存储对象的元数据信息,例如哈希码、GC 分代年龄、锁状态标志、类型指针等。
  • 实例数据(Instance Data): 存储对象的成员变量的值。
  • 对齐填充(Padding): 为了保证对象是 8 字节的倍数,可能会进行填充。

我们今天要重点关注的是对象头,特别是其中的 Mark Word

2. Mark Word 的结构

Mark Word 用于存储对象的运行时数据,它的结构会随着对象状态的变化而变化。在 32 位 JVM 和 64 位 JVM 中,Mark Word 的长度分别是 4 字节和 8 字节。不同的锁状态下,Mark Word 的结构如下:

锁状态 标志位 (2 bit) 存储内容
无锁 01 对象的 hashCode、分代年龄 (4 bit)
偏向锁 01 偏向线程 ID、epoch、分代年龄 (4 bit)、是否是可偏向 (1 bit)
轻量级锁 00 指向栈中锁记录的指针
重量级锁 10 指向互斥量(mutex)的指针
GC 标记 11 空 (用于垃圾回收)

3. 分代年龄(Age)的存储

分代年龄是垃圾回收器用来判断对象是否应该被回收的一个重要依据。在新生代 Minor GC 中,每次存活下来的对象都会被增加分代年龄。当分代年龄达到一定阈值(默认是 15),对象就会被晋升到老年代。

Mark Word 中使用 4 bit 来存储分代年龄,因此最大值为 15。

代码示例:查看对象头信息

我们可以使用 JOL (Java Object Layout) 工具来查看对象的内存布局和 Mark Word 的值。首先,引入 JOL 的 Maven 依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>

然后,编写一个简单的 Java 程序:

import org.openjdk.jol.info.ClassLayout;

public class MarkWordExample {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();

        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        // 模拟对象存活一段时间,经历几次 GC
        for (int i = 0; i < 20; i++) {
            System.gc();
            Thread.sleep(100);
        }

        System.out.println("After GC:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

运行上述代码,可以观察到对象的 Mark Word 的变化。初始状态下,Mark Word 包含对象的 hashCode 和分代年龄 (通常是 0)。经过多次 GC 后,分代年龄会增加。注意,实际的分代年龄增长取决于 JVM 的 GC 策略,这里只是模拟。

输出结果示例:

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     0        (object alignment gap)                  
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

After GC:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           0f 00 00 00 (00001111 00000000 00000000 00000000) (15)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     0        (object alignment gap)                  
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

在这个例子中,初始的 Mark Word 值是 05 00 00 00,其中低三位 101 代表无锁状态,其他位可能包含 hashCode,分代年龄初始值为0。经过多次 GC 后,Mark Word 值变为 0f 00 00 00,表示分代年龄达到了最大值 15 (1111)。

4. 偏向锁的线程 ID 的存储

偏向锁是 JVM 为了优化无竞争情况下的锁操作而引入的一种机制。当一个线程访问一个对象并成功获取锁后,Mark Word 中会记录该线程的 ID,后续该线程再次访问这个对象时,不需要进行任何同步操作,直接可以访问,大大提高了性能。

偏向锁的 Mark Word 结构如下:

字段 位数 描述
线程 ID 54/31 获取偏向锁的线程 ID (根据 JVM 位数)
epoch 2 偏向时间戳,用于撤销偏向锁
分代年龄 4 对象的 GC 分代年龄
是否可偏向 1 0:不可偏向,1:可偏向
锁标志位 2 01 (偏向锁状态)

在 64 位 JVM 中,线程 ID 占用 54 位。在 32 位 JVM 中,线程 ID 占用 31 位。

代码示例:演示偏向锁

import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

public class BiasedLockExample {
    public static void main(String[] args) throws InterruptedException {
        // 延迟一段时间,允许 JVM 启用偏向锁
        TimeUnit.SECONDS.sleep(5);

        Object obj = new Object();

        System.out.println("Before first lock:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        // 第一个线程获取锁
        synchronized (obj) {
            System.out.println("In first lock:");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }

        System.out.println("After first lock:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        // 第二个线程尝试获取锁
        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                System.out.println("In second lock:");
                System.out.println(ClassLayout.parseInstance(obj).toPrintable());
            }
        });
        t2.start();
        t2.join();

        System.out.println("After second lock:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

    }
}

代码解释:

  1. 延迟启动: TimeUnit.SECONDS.sleep(5); 这行代码是为了让 JVM 充分预热,并启用偏向锁优化。JVM 启动时,偏向锁默认是延迟启用的,可以通过 -XX:BiasedLockingStartupDelay=0 参数来禁用延迟。
  2. 第一个线程获取锁: synchronized (obj) 块中的代码,第一个线程获取了锁。此时,Mark Word 会记录该线程的 ID,并将锁状态设置为偏向锁。
  3. 第二个线程尝试获取锁: Thread t2 尝试获取同一个锁。由于已经有线程持有偏向锁,此时会发生锁撤销,升级为轻量级锁或重量级锁。

输出结果示例:

Before first lock:
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     0        (object alignment gap)                  
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

In first lock:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           45 08 00 00 (01000101 00001000 00000000 00000000) (1349)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     0        (object alignment gap)                  
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

After first lock:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           45 08 00 00 (01000101 00001000 00000000 00000000) (1349)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     0        (object alignment gap)                  
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

In second lock:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      4     4        (object header)                           90 05 00 20 (10010000 00000101 00000000 00100000) (536872848)
      8     0        (object alignment gap)                  
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

After second lock:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      4     4        (object header)                           90 05 00 20 (10010000 00000101 00000000 00100000) (536872848)
      8     0        (object alignment gap)                  
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

分析:

  • Before first lock: 对象处于无锁状态,Mark Word 的标志位为 01
  • In first lock: 第一个线程获取锁后,Mark Word 的值发生了变化,其中包含了线程 ID。根据具体的 JVM 实现和线程 ID,实际的值会有所不同。 注意标志位仍然为01,表示偏向锁状态。
  • In second lock: 由于偏向锁被竞争,锁升级为轻量级锁或者重量级锁,Mark Word 结构发生改变。

注意:

  • 实际输出的 Mark Word 值会受到 JVM 版本、操作系统、硬件环境等因素的影响。
  • 偏向锁的启用和撤销策略由 JVM 自动管理,我们无法直接控制。

5. 偏向锁的撤销

当另一个线程尝试获取已经被偏向的锁时,JVM 会撤销偏向锁。撤销偏向锁的过程如下:

  1. 暂停持有偏向锁的线程: JVM 会尝试暂停持有偏向锁的线程。
  2. 检查线程是否仍然持有锁: 如果线程已经执行完毕,则将对象头重置为无锁状态。如果线程仍然持有锁,则将对象头的 Mark Word 设置为指向锁记录的指针,升级为轻量级锁。
  3. 唤醒等待线程: 唤醒尝试获取锁的线程,让其重新尝试获取锁。

6. 总结:Mark Word 存储的关键信息

今天我们深入探讨了 Java 对象头中的 Mark Word,以及它如何存储分代年龄和偏向锁的线程 ID。Mark Word 的结构会随着对象状态的变化而变化,理解这些变化对于深入理解 JVM 的内存管理和并发机制至关重要。通过 JOL 工具,我们可以直观地观察对象的内存布局和 Mark Word 的值,从而更好地理解 JVM 的底层实现。希望今天的分享能够帮助大家更好地理解 Java 对象的内部结构和锁机制。理解这些有助于编写更高效、更健壮的 Java 代码。

发表回复

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