在Java中任意对象都可以用作锁,因此必定要有一个映射关系,存储该对象以及其对应的锁信息(比如当前哪个线程持有锁,哪些线程在等待)。一种很直观的方法是,用一个全局map,来存储这个映射关系,但这样会有一些问题:需要对map做线程安全保障,不同的synchronized
之间会相互影响,性能差;另外当同步对象较多时,该map可能会占用比较多的内存。所以最好的办法是将这个映射关系存储在对象头中,因为对象头本身也有一些hashcode、GC相关的数据,所以如果能将锁信息与这些信息共存在对象头中就好了。
在JVM中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark word
和类型指针。另外对于数组而言还会有一份记录数组长度的数据。在Java中对象包含三个部分,概括起来分为对象头、对象体和对齐字节。如果是数组,还包括数组长度。markword的结构,定义在markOop.hpp文件
32 bits:
--------
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
64 bits:
--------
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)
unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
- the two lock bits are used to describe three states: locked/unlocked and monitor.
[ptr | 00] locked ptr points to real header on stack
[header | 0 | 01] unlocked regular object header
[ptr | 10] monitor inflated lock (header is wapped out)
[ptr | 11] marked used by markSweep to mark an object
在markOop.hpp文件中定义了Mark Word部分,对象头还包括Klass Word。用图表示如下
Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例
对象头Mark Word
下图是Java对象处于5种不同状态时,Mark Word中64个bit位的表现形式
我们可以看出Java的对象头在对象的不同的状态下会有不同的表现形式,主要有三种状态,无锁状态,加锁状态,GC标记状态。那么就可以理解Java当中的上锁其实可以理解给对象上锁。也就是改变对象头的状态,如果上锁成功则进入同步代码块。其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起
偏向锁标识位 锁标识位 锁状态 0 01 未锁定 1 01 偏向锁 无 00 轻量级锁 无 10 重量级锁 无 11 GC标记 biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。(JVM的做法是将偏向锁和无锁的状态表示为同一个状态,都为01,然后根据偏向锁的标识再去标识是无锁还是偏向锁状态)
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中
thread:持有偏向锁的线程ID
epoch:偏向锁的时间戳
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针
我们通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。但是重量级锁会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。为了提高效率,不会一开始就使用重量级锁,JVM在内部会根据需要,进行锁的升级
查看对象头信息
借助JOL工具可以看到对象头在加锁状态下的信息,首先需要使用依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
创建Person.java
public class Person {
}
然后创建Demo类
public class MarkTest {
public static void main(String[] args) {
Person person = new Person();
//打印JVM的详细信息
System.out.println("---JVM的详细信息---");
System.out.println(VM.current().details());
//打印对应的对象头信息
System.out.println("---对象头信息---");
System.out.println(ClassLayout.parseInstance(person).toPrintable());
}
}
运行的结果如下
不是说Klass是64bits(8个字节)但是这儿只有4个字节,因为默认情况下是开启压缩指针的,我们可以通过JVM运行参数-XX:-UseCompressedOops
来关闭压缩指针,这样Klass就是8个字节了。我们可以看到整个对象是16个字节,其中对象头(object header)12B(12个字节),还有4B是对齐的字节(因为在64位虚拟机上对象的大小必须是8的倍数),由于这个对象里面没有任何字段,故而对象的实例数据为0B。现在的锁状态是01,表示无锁状态。注意因为是小端存储,所以你看的值是倒过来的。前25bit没有使用所以都是0,后面31bit存的hashcode,所以第一个字节中八位存储的分别就是分代年龄、偏向锁信息、对象状态
插一个题外话,我们可以证明一下对象的大小必须是8的倍数。加一个int(4个字节)数据,如下
public class Person { //占四个字节的int字段 private int age; }
结果如下
这个对象的大小还是没有改变还是16B,那是因为剩余了4个对齐字节,刚好填上了int属性,我们再在Person中加一根boolean属性看一下
public class Person { //占一个字节的boolean字段 private boolean flag; //占四个字节的int字段 private int age; }
结果如下
尽管boolean属性只占用了一个字节,但是对象大小是24字节
偏向锁
我们可以通过代码模拟触发偏向锁的情况,然后查看对象头的信息。代码如下
public class MarkTest {
private static final Person person = new Person();
public static void main(String[] args) {
//打印对应的对象头信息
System.out.println("---before lock---");
System.out.println(ClassLayout.parseInstance(person).toPrintable());
sync();
System.out.println("---after lock---");
System.out.println(ClassLayout.parseInstance(person).toPrintable());
}
private static void sync() {
synchronized (person) {
System.out.println("---thread id---"+Thread.currentThread().getId());
System.out.println("---获取到锁---");
System.out.println(ClassLayout.parseInstance(person).toPrintable());
}
}
}
结果如下
上面这个程序只有一个线程去调用sync方法,应该是偏向锁,但是你会发现输出的结果(第一个字节)依然是00000001和无锁的时候一模一样,其实这是因为虚拟机在启动的时候对于偏向锁有延迟,即偏向锁功能要在JVM启动几秒后才会激活,所以要关闭偏向锁的延迟,在JVM启动参数添加 -XX:BiasedLockingStartupDelay=0
再运行结果如下
这时候大家会有疑问了,为什么在没有加锁之前是偏向锁,准确的说,应该是叫可偏向的状态,因为它后面没有存线程的ID,当lock ing的时候,后面存储的就是线程的ID(50276357)。这里我打印了当前线程的id,发现和对象头中存储的id不一样,实际上对象头中存储的是os的线程id,而不是Thread.currentThread.getId()
得到的线程id,它是JVM分配的id,不是os的线程id。
最后我们再画个图总结下各种锁的对象头(只画出了最重要的部分,其他的省略)