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)、重入计数等大量其他字段

image-20260618104814567

hashCode() 行为速查表

核心原则

  1. hashCode() 在整个对象生命周期中只计算一次,后续全部从缓存读取。
  2. 计算出的 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()
    1. 撤销偏向锁
    2. 升级为轻量级锁或重量级锁
    3. 首次生成 hashCode 并备份到 Lock Record 或 ObjectMonitor 中

结论:JVM 绝不会在对象已有 hashCode 的情况下重新计算一个新值覆盖原值。


3. 锁升级全过程

3.1 升级路径总览

text

无锁 (01)  →  偏向锁 (01)  →  轻量级锁 (00)  →  重量级锁 (10)
      ↑              ↑               ↑                ↑
   (无竞争)      (单线程)       (少量竞争)       (激烈竞争)

重要特性:锁升级是单向的,不能降级。


3.2 阶段 1:无锁 → 偏向锁

触发条件

  • 对象刚创建,Mark Word 为无锁状态(偏向位 = 0)
  • 仅有一个线程进入 synchronized

操作流程

  1. CAS 写入:将当前线程 ID 写入 Mark Word 的偏向线程 ID 字段
  2. 设置标志位:偏向位置 1,锁标志位保持 01
  3. 不保存 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:升级为轻量级锁

  1. T1 在栈帧中创建 Lock Record
  2. 拷贝 Mark Word(含 hashCode)到 Lock Record 的 Displaced Mark Word
  3. CAS 替换 Mark Word:将对象头的 Mark Word 指向 T1 的 Lock Record
  4. 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)

  1. 从 Monitor 池分配 ObjectMonitor 对象
  2. 数据迁移
    • 将 Lock Record 中的 Displaced Mark Word(含 hashCode)
    • 迁移到 ObjectMonitor._header
  3. 初始化 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 Word
  • synchronized 会修改 Mark Word
  • 可能引发性能暴跌甚至死锁(JVM 有保护机制,但代价极高)

6.6 不同锁状态下的 Mark Word 操作对象

核心结论:所有锁升级操作都是针对具体对象实例的 Mark Word

操作 修改的目标
synchronized(obj) 修改 obj 指向的堆中对象的 Mark Word
synchronized(this) 修改当前对象的 Mark Word
synchronized(MyClass.class) 修改 MyClassClass 对象的 Mark Word
synchronized 实例方法 修改 this 对象的 Mark Word
synchronized 静态方法 修改对应 Class 对象的 Mark Word

7. 总结

7.1 核心要点

  1. Mark Word 是锁状态存储的核心,不同锁状态存储不同数据。
  2. hashCode 首次生成后即缓存,后续直接从缓存读取。
  3. 锁升级单向且不可逆:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
  4. 有 hashCode 的对象不能进入偏向锁,因为布局冲突。
  5. 所有锁操作都修改堆中对象的 Mark Word,不同对象互不干扰。
  6. 锁释放时 Mark Word 会恢复,包含原始 hashCode。

7.2 性能对比

锁类型 开销 适用场景 是否支持 wait/notify
偏向锁 极低 单线程访问
轻量级锁 少量竞争,自旋可解决
重量级锁 激烈竞争,线程阻塞
posted @ 2026-06-18 10:05  deyang  阅读(4)  评论(0)    收藏  举报