从最开始学
Java的时候,便听说int类型比Integer类型要小很多,那么究竟是小在哪里呢?Integer比int究竟多了什么东西?这是本文要着重介绍的对象头,并介绍Monitor和锁。
对象头
对象头是Java对象在内存布局中的第一部分,包含了对象的元数据信息。每个Java对象在堆内存中都由对象头 、实例数据 和对齐填充三部分组成。
下面以32位虚拟机来介绍对象头的结构:
- 普通对象
普通对象的对象头共64位,包括:
- 32位的
Mark Word:最核心的部分,存储了对象的运行时数据 - 32位的
Klass Word:指向方法区中类元数据的指针,JVM通过这个指针确定对象属于哪个类。
- 数组对象
数组对象相对于普通对象,多了一个array length字段,用于存放数组声明时的大小。
对象头的作用
对象头的关键是Mark Word字段,因此本节深入探索Mark Word
在正常的情况下,Mark Word的结构如下:
下面来介绍这些字段:
-
hashcode字段是JVM在对象内部维护的一个内部标识 。它是一个31位的整数(在32/64位系统中位置不同),也称为identity hash code。这个值是一个懒加载的,只有当JVM第一次需要用到这个内部哈希码时,才会计算并存入Mark Word:- 首次调用默认的
Object.hashCode()方法(即没有重写过的)。 - 首次调用
System.identityHashCode(obj)方法。 - 在某些锁升级(如偏向锁)等情况下,也可能被计算。
- 首次调用默认的
-
age字段和垃圾回收机制有关 -
biased_lock字段,代表偏向锁是否启用 -
剩下的两位可以理解为当前的对象锁模式。
Monitor
Monitor是JVM实现的同步机制,包含以下关键组件:
Owner:当前持有锁的线程Entry Set:等待获取锁的队列Wait Set:调用wait方法后进入等待状态的线程队列- 其他组件:
Recursion:记录锁重入的次数Count:线程获取锁的次数

只有在对象作为锁的时候,才会创建一个对应的Monitor。
当通过synchronized去对一个对象加锁时,如果该锁不被其他线程持有,将获得这把锁,Owner变成该线程,如果该锁已经被其他线程持有,将会进入EntryList阻塞。
当线程获得锁之后,该对象的Mark Word将从
变成

也就是说,对象头中的Mark word变成了一个指向monitor的指针,而原本的Mark word信息保存在了Monitor中的_header字段中。
锁升级机制
虽然代码中使用synchronized,但是实际上可能根本就不存在多线程竞争资源,因此锁升级机制就出现了。
在 HotSpot JVM 中,对象头的 Mark Word 记录了锁的状态,升级路径主要经过以下几个阶段:
- 无锁状态:一个新创建的对象,尚未被任何线程锁定。
- 偏向锁 :适用于只有一个线程反复进入同步块的场景。第一次获取锁时,会在对象头和栈帧中记录偏向的线程ID。之后该线程再进入时,只需简单检查线程ID是否匹配,无需任何原子操作(如CAS),开销极低。
- 轻量级锁 :当有少量线程交替竞争(即竞争不激烈,没有同时抢)时升级为此状态。它通过CPU的CAS操作在对象头和线程栈帧中复制、替换锁记录来实现同步,避免了操作系统级互斥量(Mutex)的阻塞开销。
- 重量级锁 :当有多线程激烈竞争 时,轻量级锁会通过自旋 尝试获取锁。若自旋失败(或达到阈值),锁会膨胀为重量级锁。此时,未获取到锁的线程会被挂起,进入操作系统的等待队列,等待被唤醒。这是开销最大的锁。
偏向锁
当如果一个锁在绝大多数时间里,都被同一个线程反复获得,那么JVM就可以让这个线程后续的加锁操作无需再进行任何同步操作(如CAS原子指令)
为了实现偏向,对象头中的Mark Word会记录第一个获得它的线程的ID ,以及一个偏向时间戳(epoch)。
当对象被创建后,如果没有禁用偏向锁,将会在一定时间后,Mark word会从
变成
最开始,thread是0,因为此时还未偏向于任何一个线程。
加锁
当一个线程T1第一次获取这个对象锁后:
- JVM检查对象是否处于可偏向状态。
- 通过一次CAS操作 ,尝试将当前线程
T1的id写入对象的Mark Word。 - 如果CAS成功 ,
thread将记录T1的id。这意味着锁偏向了线程T1。这次操作开销稍大,因为包含了CAS。
当一个线程T1再次进入这个同步块:
- JVM检查对象头中的线程
id。 - 发现就是自己(
T1),则直接通过,没有任何原子操作或系统调用。
解锁
线程T1执行完同步块,并不会主动释放偏向锁 。它不会将Mark Word中的线程ID清空。对象依然保持在 "已偏向于T1" 的状态。这为T1下次快速进入创造了条件。
锁升级
当有另一个线程T2也试图获取这个锁时:
-
撤销(Revoke) :JVM需要"主持公道",撤销对T1的偏向。这个过程必须在全局安全点进行,它会暂停拥有偏向锁的线程T1。
-
检查T1状态:
- 如果T1已经退出 同步块,则撤销完成,将对象恢复到 "可偏向状态" (但此时T2还未获得锁)。
- 如果T1仍在执行 同步块,则将偏向锁升级为轻量级锁。
轻量级锁
在大多数情况下,同步代码块在运行期间不存在竞争,如果每次加锁都使用操作系统的重量级互斥量,会造成巨大的性能开销。
加锁
当锁升级为轻量级锁后,一个线程获取锁后,Mark Word从
变成
加锁时,会在当前线程的栈帧中,创建一个LOCK RECORD,原本的Mark Word复制到LOCK RECORD中的Displaced Mark Word。
其加锁过程如下:
- 检查对象头是否是无锁状态
- 创建
LOCK RECORD - CAS更新
解锁
- 检查当前锁是否为轻量级锁(锁标志位=00)
- 从对象头获取指向锁记录的指针
- 使用CAS将对象头恢复为
Displaced Mark Word
锁升级
升级的条件是:
CAS尝试失败 && 自旋次数 > 阈值,JVM默认采用自适应自旋,自旋次数阈值是动态计算的- 第三个线程参与竞争
- 调用
wait
重量级锁
通过synchronized获取重量级锁时,synchronized会被编译为monitorenter和monitorexit指令。

重量锁的"重"和轻量锁的"轻"
重量锁之所以重,是因为它需要依赖操作系统的互斥锁 来实现线程同步,这会涉及到用户态到内核态 的切换,并且线程竞争失败会阻塞,后续获得锁后要切换线程的上下文 ,而且还需要关联一个Monitor 。
轻量级锁是完全在在用户态 完成的,竞争失败也只是自旋。