Java对象头

以32位虚拟机为例

  • 普通对象

    • Object Header 64b

      • Mark Word 32b

        • MarkWord会随着状态改变而改变

        • Mark Word (32 bits)State
          hashcode:25 | age:4 | biased_lock:0 | 01Normal
          thread: 23 | epoch: 2 | age: 4 | biased_lock:1 | 01Biased
          ptr_to_lock_record: 30 | 00Lightweight Locked
          ptr_to_heavyweight_monitor: 30 | 10Heavyweight Locked
          (blank) | 11Marked for GC
        • hashcode 哈希码

        • age 垃圾回收时分代年龄,

        • biased_lock 是不是偏向锁

        • 最后两位表示加锁状态,发是

      • Klass Word 32b 是一个指针,指向对象所存储的class,

  • 数组对象

    • Object Header 96b
      • Mark Word 32b
      • Klass Word 32b
      • array length 32b

可以看到,对象占了很多空间空间,比如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
      • 替换成功,则对象头存储了锁记录的地址,然后锁记录里面存储原对象头中的信息。以后别的线程再过来判断时,会发现它被其它线程在使用
      • 替换失败。
        1. 当其它线程把obj的Mark Word的后两位替换成00,说明已被别的线程占用,表示有锁竞争,进入锁膨胀
        2. 如果是锁重入,再添加一条Lock Record作为重入的计数。(只是不会再记录原对象头信息,为null。可以数锁记录的数量,就是重入锁的次数)

下面是解锁时:

  • 当退出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即时编译器,会对字节码进行再优化。分析局部变量是否会被共享,没有被共享就不会加锁。

逃逸分析锁消除。

如果不加,加锁就非常慢了。