AI生成-Java 对象头与锁升级机制完全解析
自己总结
1.synchronized (obj) :锁升级时修改的对象Mark Word 数据,修改的就是obj这个对象
2.hashCode()只会算一次。
- 未hashCode()过-无锁时hashCode():计算后存入对象Mark Word,然后返回
- 未hashCode()过-有偏向锁的时候hashCode():直接升级成轻量级锁,计算后存入 Lock Record,然后返回
- 未hashCode()过-有轻量级锁的时候hashCode():计算后存入 Lock Record,然后返回
- 未hashCode()过-有重量级锁的时候hashCode():计算后存入 Monitor,然后返回
- hashCode()过-无锁时hashCode():从对象Mark Word中读取返回
- hashCode()过-有偏向锁的时候hashCode():不会有这个状态,有hashCode时会直接升级成轻量级锁,因为偏向锁的时候Mark Word里面没地方存hashCode
- hashCode()过-有轻量级锁的时候hashCode():从 Lock Record中读取返回
- hashCode()过-有重量级锁的时候hashCode():从 Monitor中读取返回
3.轻量级锁的数据为什么是存在栈中Lock Record对象
- 线程私有、短生命周期、访问快、无需 GC
- 字段仅包含:备份 Mark Word
4.重量级锁的数据为什么是存在堆中的monitor对象
- 跨线程共享、长生命周期、状态复杂、支持 wait/notify
- 字段包含:备份 hashCode 等、持有锁的线程、阻塞队列、等待队列(wait/notify)、重入计数等大量其他字段

hashCode() 行为速查表
核心原则
hashCode()在整个对象生命周期中只计算一次,后续全部从缓存读取。- 计算出的 hashCode 存储位置取决于当前锁状态。
详细行为表
| 场景 | 是否已计算 hashCode | 当前锁状态 | 行为 | hashCode 存储位置 |
|---|---|---|---|---|
| 1 | ❌ 未计算 | 无锁 | 首次计算,存入 Mark Word | Mark Word |
| 2 | ❌ 未计算 | 偏向锁 | 撤销偏向锁 → 升级为轻量级锁 → 首次计算 | Lock Record |
| 3 | ❌ 未计算 | 轻量级锁 | 首次计算 | Lock Record |
| 4 | ❌ 未计算 | 重量级锁 | 首次计算 | ObjectMonitor._header |
| 5 | ✅ 已计算 | 无锁 | 直接读取 | Mark Word |
| 6 | ✅ 已计算 | 偏向锁 | 🚫 此状态不存在(有 hashCode 时 JVM 禁止进入偏向锁) | — |
| 7 | ✅ 已计算 | 轻量级锁 | 直接读取 | Lock Record(加锁时从 Mark Word 拷贝而来) |
| 8 | ✅ 已计算 | 重量级锁 | 直接读取 | ObjectMonitor._header(升级时从 Lock Record 迁移而来) |
轻量级锁的数据为什么是存在栈中Lock Record对象
重量级锁的数据为什么是存在堆中的monitor对象
一、轻量级锁:为什么存在栈中(Lock Record)?
1.1 核心原因:线程私有 + 快速访问
Thread T1 的栈(栈帧中) 堆内存
┌─────────────────────┐ ┌─────────────────────┐
│ Lock Record │ │ Object obj │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ Displaced Mark │ │◄───────│ │ Mark Word │ │
│ │ Word (备份) │ │ 指向 │ │ (指向Lock Record)│ │
│ └─────────────────┘ │ │ └─────────────────┘ │
│ │ Owner (当前线程) │ │ │ │ 实例数据 │ │
│ └─────────────────┘ │ │ └─────────────────┘ │
└─────────────────────┘ └─────────────────────┘
为什么选栈?
| 维度 | 说明 |
|---|---|
| 线程私有 | Lock Record 只属于当前持有锁的线程,不存在并发竞争,无需同步 |
| 访问速度 | 栈是 CPU 缓存友好的内存区域,访问比堆快得多 |
| 自动释放 | 栈帧随方法退出自动销毁,无需 GC 回收,避免内存泄漏 |
| 生命周期匹配 | 轻量级锁只在方法执行期间存在,和栈帧生命周期完全一致 |
1.2 如果存在堆里会怎样?(反面论证)
- ❌ 堆中的对象需要 GC 管理(分配、回收开销大)
- ❌ 多个线程可能同时访问,需要额外的同步控制
- ❌ CPU 缓存命中率低(堆内存不连续)
结论:轻量级锁是短期、线程私有的资源,放在栈中完美契合其特性。
二、重量级锁:为什么存在堆中(ObjectMonitor)?
2.1 核心原因:跨线程共享 + 长期存在
堆内存
┌─────────────────────────────────────────────┐
│ ObjectMonitor (从 Monitor 池分配) │
│ ┌─────────────────────────────────────────┐ │
│ │ _header (备份的 hashCode) │ │
│ │ _owner (当前持有锁的线程) │ │
│ │ _EntryList (等待获取锁的线程队列) │ │
│ │ _WaitSet (调用 wait() 的线程队列) │ │
│ │ _count (重入计数) │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
▲
│ 多个线程通过 Mark Word 中的指针访问
│
┌───────────┴───────────┐
│ │
线程 T1 栈 线程 T2 栈
(持有锁) (阻塞等待)
为什么选堆?
| 维度 | 说明 |
|---|---|
| 跨线程共享 | Monitor 需要被多个线程同时访问(_owner、_EntryList 等),堆是天然共享区域 |
| 生命周期长 | 重量级锁可能长期存在(涉及 wait/notify),甚至锁释放后 Monitor 可复用(对象池) |
| 状态复杂 | Monitor 包含队列、计数器、状态标志等大量字段,需要持久化存储 |
| 操作系统交互 | Monitor 底层关联操作系统的互斥量(mutex),需要稳定的内存地址供内核操作 |
2.2 如果存在栈里会怎样?(反面论证)
- ❌ 栈是线程私有的,其他线程无法访问到 Lock Record
- ❌ 锁释放后栈帧销毁,但阻塞的线程可能还在等待(生命周期不匹配)
- ❌ 无法实现
wait/notify(需要跨线程通信机制)
三、深层对比:为什么设计如此?
3.1 生命周期对比
| 锁类型 | 生命周期 | 存储位置 | 原因 |
|---|---|---|---|
| 轻量级锁 (Lock Record) | 方法执行期间(短) | 栈 | 随栈帧创建/销毁,自动管理 |
| 重量级锁 (ObjectMonitor) | 可能跨越多个方法甚至线程(长) | 堆 | 需要持久化,支持 wait/notify |
3.2 访问范围对比
| 锁类型 | 访问线程 | 存储位置 | 原因 |
|---|---|---|---|
| 轻量级锁 (Lock Record) | 仅持有锁的线程 | 栈 | 线程私有,无需同步 |
| 重量级锁 (ObjectMonitor) | 所有竞争线程 | 堆 | 跨线程共享,需要统一管理 |
3.3 状态复杂度对比
// Lock Record(轻量级锁)—— 简单
class LockRecord {
markOop displacedMark; // 仅备份 Mark Word
}
// ObjectMonitor(重量级锁)—— 复杂
class ObjectMonitor {
markOop _header; // 备份 hashCode 等
void* _owner; // 持有锁的线程
ObjectWaiter* _EntryList;// 阻塞队列
ObjectWaiter* _WaitSet; // 等待队列(wait/notify)
int _count; // 重入计数
// ... 还有大量其他字段
}
为什么复杂对象要放堆?
- 栈空间有限(默认 1MB),存不下这么复杂的数据结构
- 堆空间大,可动态分配,适合复杂对象
四、升级过程中的数据迁移(再次印证设计)
轻量级锁 → 重量级锁 时发生了什么?
1. 申请 ObjectMonitor(堆中分配)
2. 将 Lock Record(栈)中的 displacedMark(含 hashCode)
→ 迁移到 ObjectMonitor._header(堆)
3. 将对象头的 Mark Word
→ 从指向 Lock Record(栈)改为指向 ObjectMonitor(堆)
4. 等待线程从自旋 → 进入 ObjectMonitor._EntryList(堆)
为什么必须迁移?
- 栈中的 Lock Record 在线程退出后会销毁
- 但重量级锁需要长期存在,甚至持有锁的线程退出后,其他线程可能还在等待
- 所以必须把数据搬到堆中的 ObjectMonitor,保证生命周期独立于任何线程
五、一句话总结
| 锁类型 | 存储位置 | 核心理由 |
|---|---|---|
| 轻量级锁 (Lock Record) | 栈 | 线程私有、短生命周期、访问快、无需 GC |
| 重量级锁 (ObjectMonitor) | 堆 | 跨线程共享、长生命周期、状态复杂、支持 wait/notify |
设计哲学:轻量级锁是“用完即扔”的临时工具,放栈里最合适;重量级锁是需要“长期服役”的共享设施,放堆里才稳固。
1. Mark Word 结构详解
1.1 32 位 JVM Mark Word 布局
| 锁状态 | 25/24 bit | 4 bit | 1 bit | 2 bit | 说明 |
|---|---|---|---|---|---|
| 无锁 | 对象的 hashCode |
分代年龄 | 0 | 01 | 存储原始 hashCode |
| 偏向锁 | 线程 ID(54 bit) | 分代年龄 | 1 | 01 | 存储持有锁的线程 ID |
| 轻量级锁 | 指向栈中 Lock Record 的指针(30 bit) | 00 | 指向线程栈中的锁记录 | ||
| 重量级锁 | 指向 ObjectMonitor 的指针(30 bit) | 10 | 指向堆中的 Monitor 对象 |
1.2 64 位 JVM Mark Word 布局(简版)
64 位下空间更大,但逻辑一致:
| 锁状态 | 62 bit | 1 bit | 1 bit | 说明 |
|---|---|---|---|---|
| 无锁 | hashCode(31 bit)+ 其他 |
0 | 01 | 空间更充裕 |
| 偏向锁 | 线程 ID(54 bit)+ epoch | 1 | 01 | 可存储更长的线程 ID |
| 轻量级锁 | 指向 Lock Record 的指针(62 bit) | 00 | ||
| 重量级锁 | 指向 ObjectMonitor 的指针(62 bit) | 10 |
2. hashCode 的存储与获取
2.1 核心原则
Java 对象在生命周期内,
hashCode()必须返回同一个值(不修改 equals 相关字段的前提下)。
实现方式:第一次生成后立即缓存,后续直接读取缓存,而非重新计算。
2.2 hashCode 生成算法(HotSpot)
由 JVM 启动参数 -XX:hashCode 控制,共 6 种模式:
| 值 | 算法模式 | 说明 |
|---|---|---|
| 0 | Park-Miller 随机数 | 旧默认,纯随机 |
| 1 | 内存地址映射 | 将对象地址位移运算得到 |
| 2 | 固定值 1 | 仅调试用 |
| 3 | 自增序列号 | 全局递增 ID |
| 4 | 线程本地随机数种子 | JDK 8+ 默认,高性能 |
| 5 | 混合模式 | 特定架构使用 |
注意:不同 JVM 版本/参数下,同一个对象首次生成的 hashCode 可能不同,但一旦生成并缓存,就不会改变。
2.3 不同锁状态下 hashCode 的存储位置
| 锁状态 | hashCode 存储位置 | 获取方式 |
|---|---|---|
| 无锁 | Mark Word 中直接存储 | 直接从 Mark Word 读取 |
| 偏向锁 | 不支持存储 hashCode,有 hashCode 的对象无法进入偏向锁 | 如果调用 hashCode(),JVM 撤销偏向锁并升级 |
| 轻量级锁 | 备份到线程栈的 Lock Record 中 | 从 Lock Record 的 Displaced Mark Word 读取 |
| 重量级锁 | 迁移到 ObjectMonitor._header 中 |
从 ObjectMonitor 的 _header 字段读取 |
2.4 hashCode 与偏向锁的冲突
关键规则:
- 如果一个对象已经计算过 hashCode,JVM 禁止其进入偏向锁状态。
- 如果一个对象已经处于偏向锁状态,此时调用
hashCode():- 撤销偏向锁
- 升级为轻量级锁或重量级锁
- 首次生成 hashCode 并备份到 Lock Record 或 ObjectMonitor 中
结论:JVM 绝不会在对象已有 hashCode 的情况下重新计算一个新值覆盖原值。
3. 锁升级全过程
3.1 升级路径总览
text
无锁 (01) → 偏向锁 (01) → 轻量级锁 (00) → 重量级锁 (10)
↑ ↑ ↑ ↑
(无竞争) (单线程) (少量竞争) (激烈竞争)
重要特性:锁升级是单向的,不能降级。
3.2 阶段 1:无锁 → 偏向锁
触发条件
- 对象刚创建,Mark Word 为无锁状态(偏向位 = 0)
- 仅有一个线程进入
synchronized块
操作流程
- CAS 写入:将当前线程 ID 写入 Mark Word 的偏向线程 ID 字段
- 设置标志位:偏向位置 1,锁标志位保持
01 - 不保存 hashCode:偏向锁没有空间存储 hashCode
Mark Word 变化
text
[hashCode | age | 0 | 01] → [Thread ID | epoch | age | 1 | 01]
无锁状态 偏向锁状态
性能特点
- 同一线程再次进入同步块:仅检查 Thread ID,无需 CAS
- 开销极小
3.3 阶段 2:偏向锁 → 轻量级锁
触发条件
- 其他线程尝试获取该偏向锁(出现竞争)
- 对象调用
hashCode()(需要腾出空间) - 达到批量重偏向/撤销阈值
操作流程(以 T1 持有偏向锁,T2 尝试获取为例)
步骤 1:撤销偏向锁
- 等待所有线程到达安全点(Safepoint)
- 检查 T1 是否存活:
- T1 已死亡 → 恢复为无锁状态
- T1 存活 → 升级为轻量级锁
步骤 2:升级为轻量级锁
- T1 在栈帧中创建 Lock Record
- 拷贝 Mark Word(含 hashCode)到 Lock Record 的
Displaced Mark Word - CAS 替换 Mark Word:将对象头的 Mark Word 指向 T1 的 Lock Record
- T2 尝试获取:
- 创建自己的 Lock Record
- CAS 失败(被 T1 占用)
- 进入自旋等待
Mark Word 变化
text
[Thread ID | epoch | age | 1 | 01] → [ptr_to_Lock_Record | 00]
偏向锁状态 轻量级锁状态
数据迁移
text
Mark Word 中的 hashCode → 拷贝到 T1 栈帧的 Lock Record
性能特点
- 自旋等待:竞争线程循环尝试获取锁
- 用户态操作:不涉及操作系统,性能高
3.4 阶段 3:轻量级锁 → 重量级锁
触发条件
- 自旋次数超过阈值(自适应自旋由 JVM 动态调整)
- 自旋线程数超过 CPU 核心数的一半
- 线程在自旋过程中被挂起(如 GC)
操作流程
步骤 1:锁膨胀(Inflate)
- 从 Monitor 池分配
ObjectMonitor对象 - 数据迁移:
- 将 Lock Record 中的
Displaced Mark Word(含 hashCode) - 迁移到
ObjectMonitor._header
- 将 Lock Record 中的
- 初始化 Monitor 字段:
_owner= 当前持有锁的线程_EntryList、_WaitSet初始化为空
步骤 2:替换 Mark Word
- CAS 将对象头的 Mark Word 替换为指向
ObjectMonitor的指针 - 锁标志位变为
10
步骤 3:线程阻塞
- 竞争线程不再自旋
- 进入操作系统的同步队列,被挂起(阻塞)
Mark Word 变化
text
[ptr_to_Lock_Record | 00] → [ptr_to_ObjectMonitor | 10]
轻量级锁状态 重量级锁状态
数据迁移
text
Lock Record 中的 hashCode → 移动到 ObjectMonitor._header
性能特点
- 线程阻塞/唤醒:涉及内核态切换,开销大
- 支持 wait/notify:重量级锁支持
Object.wait()/notify() - 轻量级锁不支持这些方法
4. 锁升级完整流程图
5. 关键数据流向总结
5.1 锁释放时的 Mark Word 恢复
| 锁类型 | 释放操作 | 恢复方式 |
|---|---|---|
| 偏向锁 | 解锁 | 无需操作 Mark Word(下次获取时重新偏向) |
| 轻量级锁 | 解锁 | 将 Lock Record 中的 Displaced Mark Word 写回 对象头 |
| 重量级锁 | 解锁 | 将 ObjectMonitor._header 写回 对象头 |
5.2 hashCode 的数据流向
text
首次调用 hashCode():
┌─────────────────────────────────────────────────────┐
│ 无锁状态: 直接存入 Mark Word │
│ 偏向锁状态: 撤销偏向锁 → 存入 Lock Record/Monitor │
└─────────────────────────────────────────────────────┘
↓
锁升级:
┌─────────────────────────────────────────────────────┐
│ 偏向锁 → 轻量级锁: Mark Word → Lock Record │
│ 轻量级锁 → 重量级锁: Lock Record → Monitor._header│
└─────────────────────────────────────────────────────┘
↓
锁释放:
┌─────────────────────────────────────────────────────┐
│ 轻量级锁解锁: Lock Record → Mark Word │
│ 重量级锁解锁: Monitor._header → Mark Word │
└─────────────────────────────────────────────────────┘
6. 高频面试题与避坑指南
6.1 为什么有 hashCode 的对象无法进入偏向锁?
答:偏向锁的 Mark Word 布局需要存储线程 ID(54 bit),没有位置存储 hashCode。如果对象已有 hashCode,JVM 强制走轻量级锁,避免数据冲突。
6.2 轻量级锁释放后,Mark Word 如何恢复?
答:解锁时,JVM 将 Lock Record 中备份的 Displaced Mark Word(包含原始 hashCode)原样写回对象头的 Mark Word,恢复到无锁状态。
6.3 重量级锁释放后,Mark Word 如何恢复?
答:解锁时,将 ObjectMonitor._header(包含原始 hashCode)写回对象头的 Mark Word。ObjectMonitor 本身不会销毁,会被放入空闲池复用。
6.4 锁升级能降级吗?
答:不能。一旦升级为重量级锁,即使后续没有竞争,也不会降级为轻量级锁或偏向锁。这是为了避免频繁升降导致的开销。
6.5 避坑:不要在 hashCode() 中使用 synchronized(this)
java
// ❌ 错误示例
@Override
public int hashCode() {
synchronized (this) { // 危险!
return Objects.hash(field1, field2);
}
}
// ✅ 正确示例
@Override
public int hashCode() {
return Objects.hash(field1, field2); // 基于 final/不可变字段计算
}
原因:
hashCode()内部需要读取 Mark Wordsynchronized会修改 Mark Word- 可能引发性能暴跌甚至死锁(JVM 有保护机制,但代价极高)
6.6 不同锁状态下的 Mark Word 操作对象
核心结论:所有锁升级操作都是针对具体对象实例的 Mark Word。
| 操作 | 修改的目标 |
|---|---|
synchronized(obj) |
修改 obj 指向的堆中对象的 Mark Word |
synchronized(this) |
修改当前对象的 Mark Word |
synchronized(MyClass.class) |
修改 MyClass 的 Class 对象的 Mark Word |
synchronized 实例方法 |
修改 this 对象的 Mark Word |
synchronized 静态方法 |
修改对应 Class 对象的 Mark Word |
7. 总结
7.1 核心要点
- Mark Word 是锁状态存储的核心,不同锁状态存储不同数据。
- hashCode 首次生成后即缓存,后续直接从缓存读取。
- 锁升级单向且不可逆:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
- 有 hashCode 的对象不能进入偏向锁,因为布局冲突。
- 所有锁操作都修改堆中对象的 Mark Word,不同对象互不干扰。
- 锁释放时 Mark Word 会恢复,包含原始 hashCode。
7.2 性能对比
| 锁类型 | 开销 | 适用场景 | 是否支持 wait/notify |
|---|---|---|---|
| 偏向锁 | 极低 | 单线程访问 | ❌ |
| 轻量级锁 | 低 | 少量竞争,自旋可解决 | ❌ |
| 重量级锁 | 高 | 激烈竞争,线程阻塞 | ✅ |
浙公网安备 33010602011771号