本文将从底层原理和源代码层面详细解释Java的对象头(Object Header),并且尽量用通俗易懂的语言让初学者也能理解。首先从概念开始,逐步深入到实现细节,涵盖对象头的结构、作用、源码分析,并提供完整的步骤和推导。内容清晰、结构化,避免过于晦涩的技术术语。由于对象头是Java锁机制(如synchronized)的基础,我会适当结合锁的场景来增强理解。
在Java中,每个对象(比如new Object()创建的对象)在内存中不仅存储了它的实际数据(字段值),还有一个额外的“标签”部分,称为对象头(Object Header)。你可以把对象头想象成一个身份证,记录了对象的身份信息和状态,比如:
对象头就像一个“管理面板”,JVM(Java虚拟机)通过它来管理对象的生命周期、锁状态和内存分配。
对象头的主要作用是为JVM提供元数据(Metadata),支持以下功能:
没有对象头,JVM就无法高效管理对象,也无法实现多线程的线程安全。
Java对象头的结构在JVM实现中(以HotSpot JVM为主)分为几个部分,主要包括:
以下是对象头在内存中的典型布局(以64位JVM为例,假设未开启指针压缩):
部分 | 大小(64位JVM) | 描述 |
---|---|---|
Mark Word | 8字节(64位) | 锁状态、哈希码、GC年龄等 |
Class Metadata Address | 8字节(64位) | 指向类的元数据地址 |
Array Length | 4字节(可选) | 数组长度(仅数组对象有) |
Mark Word 是对象头中最复杂、最动态的部分。它的内容会根据对象状态(无锁、锁住、GC标记等)变化。Mark Word 通常包含以下信息:
hashCode()
值。Mark Word 的结构会根据锁状态动态调整。例如,在无锁状态下,它会存储哈希码和GC年龄;在锁住状态下,它会存储线程ID或Monitor指针。
以下是 Mark Word 在不同状态下的典型布局(64位JVM,未压缩指针):
锁状态 | 63-56bit | 55-2bit | 1-0bit(锁标志) |
---|---|---|---|
无锁 | GC年龄 | 对象的哈希码 | 01 |
偏向锁 | 线程ID | Epoch | 01 |
轻量级锁 | 指向锁记录的指针 | 00 | |
重量级锁 | 指向Monitor的指针 | 10 | |
GC标记 | GC相关信息 | 11 |
状态 | 位范围 | 字段名 | 大小(位) | 说明 |
---|---|---|---|---|
无锁 | 63-31 | 未使用 | 33 | 保留位,通常为 0,未来可能扩展使用。 |
30-8 | 哈希码 (hash) | 23 | 对象的 hashCode() 值,调用 System.identityHashCode() 时生成。 | |
7-4 | 分代年龄 (age) | 4 | 垃圾回收年龄,记录对象经历的 Minor GC 次数(最大 15)。 | |
3-2 | 未使用 | 2 | 保留位,通常为 0。 | |
1-0 | 锁标志位 | 2 | 01 ,表示无锁状态。 |
|
偏向锁 | 63-56 | 未使用 | 8 | 保留位,通常为 0。 |
55-8 | 线程 ID | 48 | 持有偏向锁的线程 ID,标识哪个线程“偏向”这个对象。 | |
7-6 | Epoch | 2 | 偏向锁的时间戳,用于批量撤销偏向锁(优化机制)。 | |
5-4 | 分代年龄 (age) | 4 | 同无锁状态,记录 GC 年龄。 | |
3 | 偏向锁标志 | 1 | 1 ,表示是偏向锁(与无锁区分)。 |
|
2-1 | 未使用 | 2 | 保留位,通常为 0。 | |
0 | 锁标志位 | 1 | 1 ,与偏向锁标志一起组成 01 (最低 2 位)。 |
|
轻量级锁 | 63-2 | 锁记录指针 | 62 | 指向线程栈中的锁记录(Displaced Header),保存原来的 Mark Word。 |
1-0 | 锁标志位 | 2 | 00 ,表示轻量级锁状态。 |
|
重量级锁 | 63-2 | Monitor 指针 | 62 | 指向 ObjectMonitor 实例(操作系统级互斥锁)。 |
1-0 | 锁标志位 | 2 | 10 ,表示重量级锁状态。 |
|
GC 标记 | 63-2 | GC 相关信息 | 62 | 存储垃圾回收标记信息(如对象是否存活,具体由 GC 算法决定)。 |
1-0 | 锁标志位 | 2 | 11 ,表示 GC 标记状态。 |
32位JVM中是这么存的:
锁状态 |
25bit |
4bit |
1bit |
2bit |
|
23bit |
2bit |
是否偏向锁 |
锁标志位 |
||
无锁 |
对象的哈希码 |
分代年龄 |
0 |
01 |
|
偏向锁 |
线程ID |
Epoch |
分代年龄 |
1 |
01 |
轻量级锁 |
指向栈中锁记录的指针 |
00 |
|||
重量级锁 |
指向重量级锁的指针 |
10 |
|||
GC标记 |
空 |
11 |
通俗解释:
这部分是一个指针,指向对象所属类的元数据(Class Metadata),存储在JVM的方法区(或元空间)。元数据包含类的结构信息,比如:
通俗解释:
如果对象是数组(比如int[ ]),对象头会额外包含一个4字节的字段,记录数组的长度。普通对象没有这一部分。
通俗解释:
对象头是synchronized锁的核心,因为它存储了锁状态和Monitor信息。以下是synchronized锁的工作原理与对象头的关联:
无锁状态:
01
。偏向锁:
01
。轻量级锁:
00
。重量级锁:
10
。通俗例子:
Mark Word 中的分代年龄(GC Age)用于JVM的分代垃圾回收(Generational GC):
11
)在GC期间用于标记对象是否存活。通俗解释:
当调用对象的hashCode()方法时,JVM将哈希码存储在Mark Word中。如果对象被锁住,哈希码可能被暂时移到Monitor或锁记录中。
通俗解释:
对象头的实现主要在HotSpot JVM的C++代码中,位于src/hotspot/share/oops/目录
。我们重点分析Mark Word和相关逻辑。
在HotSpot JVM中,Mark Word 由markOop类表示,定义在markOop.hpp中:
// src/hotspot/share/oops/markOop.hpp
class markOopDesc : public oopDesc {
private:
uintptr_t _value; // Mark Word的实际值(64位机器上是64位)
public:
// 获取锁状态
inline uintptr_t lock_bits() const {
return (_value & lock_mask_in_place);
}
// 获取线程ID(偏向锁)
inline uintptr_t biased_thread_id() const {
return (_value >> biased_lock_thread_id_shift);
}
// 获取哈希码
inline uintptr_t hash() const {
return (_value >> hash_shift) & hash_mask;
}
// 获取分代年龄
inline uintptr_t age() const {
return (_value >> age_shift) & age_mask;
}
};
关键点解释:
_value 是一
个64位整数,存储Mark Word的所有信息。>>
、&
)提取锁状态、线程ID、哈希码、年龄等。01
、00
、10
、11
)。HotSpot JVM中,对象的内存布局由oopDesc类定义,位于oop.hpp:
// src/hotspot/share/oops/oop.hpp
class oopDesc {
private:
volatile markOop _mark; // Mark Word
Klass* _metadata; // Class Metadata Address
};
_mark
:Mark Word,存储锁状态等。_metadata
:指向类元数据的指针。如果对象是数组,还会额外包含数组长度字段(由JVM在分配内存时添加)。
锁状态的切换在 synchronizer.cpp 中实现,涉及偏向锁、轻量级锁、重量级锁的转换。以下是简化逻辑:
// src/hotspot/share/runtime/synchronizer.cpp
void ObjectSynchronizer::enter(Handle obj, BasicLock* lock, Thread* self) {
markOop mark = obj->mark(); // 获取Mark Word
if (mark->is_neutral()) { // 无锁状态
// 尝试偏向锁
if (UseBiasedLocking) {
markOop biased = mark->biased_to(self); // 设置线程ID
if (Atomic::cmpxchg(biased, obj->mark_addr(), mark) == mark) {
return; // 偏向成功
}
}
// 尝试轻量级锁
lock->set_displaced_header(mark); // 保存Mark Word到锁记录
if (Atomic::cmpxchg((markOop)lock, obj->mark_addr(), mark) == mark) {
return; // 轻量级锁成功
}
}
// 升级到重量级锁
ObjectMonitor* monitor = inflate_monitor(obj, self);
monitor->enter(self); // 进入Monitor
}
关键点解释:
cmpxchg
)将线程ID写入Mark Word。重量级锁依赖ObjectMonitor类(objectMonitor.hpp),Mark Word存储Monitor指针:
class ObjectMonitor {
private:
Thread* _owner; // 持有锁的线程
markOop _header; // 保存原来的Mark Word
// ...
};
在64位JVM中,对象头的典型大小:
这意味着即使一个空对象(无字段)也有16字节的开销,主要来自对象头。
为了减少内存开销,HotSpot JVM支持指针压缩(-XX:+UseCompressedOops):
通俗解释:
对象头的锁状态切换(偏向锁 → 轻量级锁 → 重量级锁)是JVM的性能优化:
对象头的定义:
对象头的作用:
底层实现:
内存优化:
生活化比喻: