Java Object Header 和 锁

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)
对象头中则存有 锁状态标示等各种信息,JVM 的锁优化既是基于此。

Java 对象内容分布

对象头

HotSpot虚拟机的对象头包括两部分信息:

第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)GC分代年龄锁状态标志偏向线程ID偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为MarkWord

对象头的另外一部分是klass,类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

对齐填充

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

codecraft —— Java对象结构及大小计算

Java Object Header 结构

以下内容来自: https://gist.github.com/arturmkrtchyan/43d6135e8a15798cc46c

32 位对象头

在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|----------------------------------------------------------------------------------------|--------------------|
| Object Header (64 bits) | State |
|-------------------------------------------------------|--------------------------------|--------------------|
| Mark Word (32 bits) | Klass Word (32 bits) | |
|-------------------------------------------------------|--------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
|-------------------------------------------------------|--------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
|-------------------------------------------------------|--------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | OOP to metadata object | Lightweight Locked |
|-------------------------------------------------------|--------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | OOP to metadata object | Heavyweight Locked |
|-------------------------------------------------------|--------------------------------|--------------------|
| | lock:2 | OOP to metadata object | Marked for GC |
|-------------------------------------------------------|--------------------------------|--------------------|
  • identity_hashcode: 保存对象的哈希码
  • age: 保存对象的分代年龄
  • biased_lock: 偏向锁标识位
  • lock: 锁状态标识位
  • thread: 保存持有偏向锁的线程ID
  • epoch: 保存偏向时间戳
  • OOP: 普通对象指针(Ordinary Object Pointer)

64 位对象头

在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|------------------------------------------------------------------------------------------------------------|--------------------|
| Object Header (128 bits) | State |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| Mark Word (64 bits) | Klass Word (64 bits) | |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | Lightweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | Heavyweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| | lock:2 | OOP to metadata object | Marked for GC |
|------------------------------------------------------------------------------|-----------------------------|--------------------|

开启指针压缩的 64 位对象头

64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节
指针压缩可以通过参数 -XX:+UseCompressedOops 开启。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|--------------------------------------------------------------------------------------------------------------|--------------------|
| Object Header (96 bits) | State |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| Mark Word (64 bits) | Klass Word (32 bits) | |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| unused:25 | identity_hashcode:31 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| thread:54 | epoch:2 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_lock_record | lock:2 | OOP to metadata object | Lightweight Locked |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_heavyweight_monitor | lock:2 | OOP to metadata object | Heavyweight Locked |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| | lock:2 | OOP to metadata object | Marked for GC |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|

各个状态下的对象头

锁状态 存储内容 偏向锁标示位 锁标示位
无锁 对象哈希码、对象分代年龄 0 01
偏向锁 偏向线程ID、偏向时间戳、对象分代年龄 1 01
轻量级锁 指向锁记录的指针 00
重量级锁 执行重量级锁定的指针 10
GC标志 11

锁升级过程

JDK中对Synchronized做的种种优化,其核心都是为了减少获得锁和释放锁所带来的性能消耗,因此引入了偏向锁轻量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

偏向锁

大多数情况,对象不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,场景是只有一个线程多次访问同步块。原理是通过CAS把线程ID写入锁对象的Mark Word中,当下次获取锁时发现线程ID相同时,则无需同步,否则撤销偏向,根据情况进入轻量级锁。

获取偏向锁

  1. 线程要获取某个对象的锁时,访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认是否为可偏向状态。
  2. 如果为可偏向状态,则比对线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行步骤5;如果竞争失败,执行步骤4
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
  5. 执行同步代码。

释放偏向锁

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为01)或轻量级锁(标志位为00)的状态。

偏向锁参数

  • -XX:+UseBiasedLocking 开启偏向锁
  • -XX:BiasedLockingStartupDelay=0 设置虚拟机一启动就启动偏向锁模式,默认情况下,虚拟机启动4s之后才会启动偏向锁模式
  • -XX:-UseBiasedLocking 关闭偏向锁

轻量级锁

轻量级锁是由偏向所升级而来,在一个线程进入同步块的情况下(该线程占有偏向锁),当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。轻量级锁适合多个线程轮流进入同步块的情况。

轻量级锁的加锁过程

  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为01状态,是否为偏向锁为0),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word
  2. 拷贝锁对象头中的Mark Word复制到锁记录中
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将锁对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向锁对象的Mark Word。如果更新成功,则执行步骤4,否则执行步骤5
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为00,即表示此对象处于轻量级锁定状态。
  5. 如果这个更新操作失败了,虚拟机首先会检查锁对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的释放

  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word
  2. 如果替换成功,整个同步过程就完成了
  3. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

重量级锁

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为重量级锁

其它锁优化

适应性自旋(Adaptive Spinning)

从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少

锁粗化(Lock Coarsening)

锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。如:

1
2
3
4
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

锁消除(-XX:+EliminateLocks)

锁消除即删除不必要的加锁操作。根据 代码逃逸分析技术(-XX:+DoEscapeAnalysis),如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

1
2
3
public String append(String str1, String str2) {
return new StringBuffer().append(str1).append(str2).toString();
}

虽然StringBufferappend是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。

参考