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 策略和进行性能调优至关重要。