Java对象头
以32位虚拟机为例
-
普通对象
-
Object Header 64b
-
Mark Word 32b
-
MarkWord会随着状态改变而改变
-
Mark Word (32 bits) State hashcode:25 | age:4 | biased_lock:0 | 01 Normal thread: 23 | epoch: 2 | age: 4 | biased_lock:1 | 01 Biased ptr_to_lock_record: 30 | 00 Lightweight Locked ptr_to_heavyweight_monitor: 30 | 10 Heavyweight Locked (blank) | 11 Marked for GC -
hashcode 哈希码
-
age 垃圾回收时分代年龄,
-
biased_lock 是不是偏向锁
-
最后两位表示加锁状态,发是
-
-
Klass Word 32b 是一个指针,指向对象所存储的class,
-
-
-
数组对象
- Object Header 96b
- Mark Word 32b
- Klass Word 32b
- array length 32b
- Object Header 96b
可以看到,对象占了很多空间空间,比如Integer,它需要8字节对象头+4字节的值,而int只是4字节,在内存敏感的场景下,用int会更好。
Monitor(锁)
它被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象加锁(重量级)后,该对象的Mark Word中就被设置指向Monitor对象的指针。
就是上面Mark Word的Heavyweight Locked状态,有30b的指针,指向监视器。
而Monitor里面也有很多属性,比如WaitSet、EntryList、Owner。
当线程加锁后,会根据指针找到Monitor,并把Owner设为当前线程。
当别的线程过来访问时,会去obj的mark word找Monitor,发现Owner不是它,不能用,就会被加入到EntryList的等待队列中,然后该线程进BLOCKED状态。
其它的线程如果还来,一样会进入EntryList等待队列中,同上。
当Owner从临界区离开后,会把EntryList的线程唤醒,看他们竞争,竞争成功,则成功抢夺锁,改变Owner。
注意噢,上锁时,如果不是同一个锁(即同一个Monitor),就没有这种状态。
如果没锁,也不会关联Monitor,就没这规则。
字节码指令
synchronized锁一个对象obj:
monitorenter
:将obj对象的MarkWord为Monitor指针
执行完之后,就执行monitorexit
:将obj对象的MarkWord重置,唤醒EntryList的线程(并赋值回hashcode、age等等)
(还有一个exception table,如果出异常了,会跳到某个位置继续执行,然后释放锁)
synchronized锁升级
轻量级锁
如果一个对象虽然有多线程访问,但是多线程访问的时间是错开的(就是没有竞争的),可以使用轻量级锁。
如果有竞争,轻量级锁会升级成重量级锁。
看看下面这段代码的调用
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
// 同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
// 同步块B
}
}
下面是加锁时:
- 创建锁记录(Lock Record)对象,每个线程的栈帧会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。
- obj的结构:对象头的Mark Word。(准备变成轻量级锁:32位:
ptr_to_lock_record: 30 | 01
) - Lock Record里面有
- Object reference:对象引用,即是指向哪个加锁对象的,这里是obj
- 尝试用CAS替换obj的Mark Word(即上面的
ptr_to_lock_record
) - 替换成功,则对象头存储了锁记录的地址,然后锁记录里面存储原对象头中的信息。以后别的线程再过来判断时,会发现它被其它线程在使用
- 替换失败。
- 当其它线程把obj的Mark Word的后两位替换成00,说明已被别的线程占用,表示有锁竞争,进入锁膨胀
- 如果是锁重入,再添加一条Lock Record作为重入的计数。(只是不会再记录原对象头信息,为null。可以数锁记录的数量,就是重入锁的次数)
- obj的结构:对象头的Mark Word。(准备变成轻量级锁:32位:
下面是解锁时:
- 当退出synchronized代码块时,如果有取值为null的锁记录,表示有重入,此时重置锁记录,重入次数-1
- 当其不为null时,这里使用CAS把Mark Word的值恢复给对象头。(就是把hashcode, age)
- 成功,则解锁成功
- 失败,说明轻量级锁已经升级为重量级锁,进入重量级锁的解锁流程。
锁膨胀
当加轻量级锁过程中,CAS操作失败了,表示有竞争,就将其升级为重量级锁。
比如,Thread0已经申请到轻量级锁,此时Thread1过来竞争,加轻量锁失败,进入锁膨胀流程。
-
Thread1为obj对象申请Monitor锁,让obj的Mark Word指向重量级锁的地址。
-
然后Thread1进入Monitor的EntryList,进入阻塞
此时,Thread0执行完成后,想退出同步代码块,用CAS恢复Mark Word对象头,失败了,就会进入重量级锁的解锁流程。按照Monitor的地址找到Monitor对象,设置Owner为null,唤醒EntryList里面BLOCKED的线程。
自旋优化
就是重量级锁竞争时,不要马上进入阻塞,使用自旋进行优化,如果自旋成功(即持锁线程退出了同步代码块,释放了锁),就可以去竞争锁,而不用进入阻塞。
在Java6之后自旋锁是自适应的,非常智能,在多核CPU自旋能发挥优势(而单核是浪费)。
偏向锁
轻量级锁在没有竞争时,每次重入仍需要执行CAS操作。(而且还要创建多个锁记录)
在Java6中引入了偏向锁来做优化:只有第一次使用CAS将线程ID设置到对象头的Mark Word头,之后发现这个线程ID是自己的,就表示没有竞争,不需要重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
偏向状态
回看对象头的格式与状态。
-
如果开启的偏向锁(默认开启),对象创建后,Mark Word值的后3位是101.其它都是0
-
但是,它默认是延迟的,不会在程序启动时生效,可以改JVM参数禁用延时,也等一会再看看。
-
-XX:BiasedLockingStartupDelay=0
-
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.16</version> </dependency>
-
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x2000ef95
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
还能禁用偏向锁:-XX:-UseBiasedLocking
,这时马上进入偏向锁。
撤消偏向锁
-
有一个很诡异的情况:当调用hashcode后,会从偏向状态变回正常状态。因为没有地方放hashcode了,存储了threadid。
-
其它线程使用对象。会把偏向锁升级为轻量级锁
-
调用wait/notify (因为这个只有重量级锁才有)
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,此时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID。
(撤消后,会升级成轻量级锁,但是)
当撤销偏向锁阈值超过20次后,JVM会觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程。
##### 批量撤消
当撤销的偏向锁阈值超过40次后,JVM会觉得,自己确实偏向错了,根本不该偏向。因此整个类的所有对象都会变成不可偏向的,新建的对象也是不可偏向的。
就是竞争非常激烈,不再使用偏向锁优化了,老是改ThreadID也没用。
新的对象,不会再进入可偏向,直接是正常状态 ;竞争激烈的对象,锁升级,变成轻量级锁。
锁消除
一个方法直接++,另一个方法中,创建一个obj,然后synchronized(obj)再++,哪个方法快?
JIN即时编译器,会对字节码进行再优化。分析局部变量是否会被共享,没有被共享就不会加锁。
逃逸分析锁消除。
如果不加,加锁就非常慢了。