由JAVA对象布局到锁的原理

由JAVA对象布局到锁的原理

前言

本文基于64位虚拟机做测试

对象的内存布局

hotspot的虚拟机中,对象的内存布局包括三个部分,对象头,实例数据,对齐填充

对象头 ObjectHeader(12Byte)

下面英文摘录自hotspot

Common structure at the beginning of every GC-managed heap object.
Includes fundamental information about the heap objects layout, type,
GC state, synchronization state, and identity hash code.
Consists of two words. In arrays it is immediately followed by a length field.
 Note that both Java objects and VM-internal objects have a common object header format.
GC管理的对象的公共的数据结构
包含对象布局,类型,GC状态,锁状态,hashcode
由两部分组成第一部分Mark Word,第二部分klass pointer

Mark word(8Byte)

The first word of every object header.
Usually a set of bitfields including synchronization state and identity hash code. 
May also be a pointer to synchronization related information.
During GC, may contain GC state bits.
对象头的第一部分
包含锁状态,hashcode
也可以是指向同步相关信息的指针
可能包含gc状态

下面是openJDK的源码markOop.hpp对于64位虚拟机Mark Word描述的片段

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)

详细一点的Mark Word

无锁  unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2
偏向  thread:54             | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2
轻量 ptr_to_lock_record:62												  | lock:2
重量 ptr_to_heavyweight_moniter:62										  | lock:2
GC  空																	  | lock:2

我们着重需要了解下面8bits

unused:1     age:4    biased_lock:1    lock:2

在这里插入图片描述
age占四位是因为晋升老年代的临界年龄是15岁
偏向标记1代表偏向锁,0代表不是偏向锁
对象状态

MarkWord存储内容       偏向标记  对象状态     状态
hashcode,age           0      01          无锁
偏向线程id,age          1      01 		  偏向锁
指向锁记录的指针				   00		  轻量锁
指向重量级锁的指针				   10 		  重量锁
空							   11          GC

重量锁:使用操作系统mutex_lock来实现的传统锁(有竞争,多个线程同时请求进入临界区)
轻量锁:没有资源竞争的时候使用CAS消除同步使用的互斥量(无竞争,多个线程交替进入临界区,轻量级锁退出同步块后变成无锁)
偏向锁:没有资源竞争的时候把整个同步消除,连CAS也不做(无竞争,只有一个线程进入临界区,偏向锁退出同步块还是偏向锁)

klass pointer(4Byte)

The second word of every object header.
Points to another object (a metaobject)
which describes the layout and behavior
of the original object.
对象头的第二部分
类型指针指向类元数据
JVM通过这个指针确定该对象是哪个类的实例

实例数据

对象真正存储起来的有效信息

对齐填充

为了保证对象大小是8的整数倍

测试对象内存布局

添加依赖,没用的话就用jar包

	<dependency>
       <groupId>org.openjdk.jol</groupId>
       <artifactId>jol‐core</artifactId>
       <version>0.8</version>
    </dependency>

A.class

public class A {
    long i;
}

测试

	public static void main(String[] args) throws Exception {
         A a = new A();//猜一猜对象a占多少字节(对象头+实例数据+填充)
         System.out.println(ClassLayout.parseInstance(a).toPrintable());
     }

输出结果

com.lry.basic.sync.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (alignment/padding gap)                  
     16     8   long A.i                                       0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

结果分析
对象头=4*3=12字节
实例数据=8字节
填充=4字节
a=24字节
第一行和第二行的对象头是Mark Word,第三行是Klass pointer
我们着重于分析unused:1 age:4 biased_lock:1 lock:2
由于我电脑系统是小端模式,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,
所以这8个字节实际上是00000001,而不是对象头第二行的最后一个字节00000000
由00000001可知我们对象状态是01,偏向标记=0,处于无锁状态。

轻量级锁与偏向锁的性能对比

先看轻量级锁
A

public class A {
    long i;
    public  synchronized void add(){
        ++i;
    }
}
	public static void main(String[] args) throws Exception {  
         A a = new A();
         long start = System.currentTimeMillis();
         //没有竞争,偏向锁开启有4s左右延迟,所以这里是轻量级锁
     	//如果启用偏向锁的话,偏向锁会有几秒的延迟,这是为了在启动JVM时候降低资源消耗的一种措施
         for(int i=0;i<1000000000;i++)
         a.add();
         long end = System.currentTimeMillis();
         System.out.println(String.format("%sms", end-start));
     }

结果是17747ms

再看偏向锁

public static void main(String[] args) throws Exception {  
		 Thread.sleep(5000);//等待5s 度过4s偏向锁的延迟
         A a = new A();
         long start = System.currentTimeMillis();
         //没有竞争,偏向锁已经开启,所以这里是偏向锁
         for(int i=0;i<1000000000;i++)
         a.add();
         long end = System.currentTimeMillis();
         System.out.println(String.format("%sms", end-start));
     }

结果是1336ms
还有一种方法可以不睡眠5s,加入-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0参数效果一样。
我偶然的看到了openJDK关于偏向锁的源码biasedLocking.cpp

	void BiasedLocking::init() {
        if (UseBiasedLocking) {
             if (BiasedLockingStartupDelay > 0) {
                     EnableBiasedLockingTask* task = new EnableBiasedLockingTask(BiasedLockingStartupDelay);
                     task->enroll();
             } else {
                    VM_EnableBiasedLocking op(false);
                    VMThread::execute(&op);
             }
           }
        }

重量级锁的性能

A

public class A {
    long i;
    public  synchronized void add(){
        ++i;
        Sync1.cdl.countDown();
    }
}

Sync1

public class Sync1 {
    final static CountDownLatch cdl = new CountDownLatch(1000000000);
    public static void main(String[] args) throws Exception {
         final A a = new A();
         long start = System.currentTimeMillis();
         
         for(int i=0;i<2;i++)//资源竞争
         new Thread(new Runnable() {
             @Override
             public void run() {
                 while(cdl.getCount()>0)
                     a.add();
             }
         }).start();
        cdl.await();
        
         long end = System.currentTimeMillis();
         System.out.println(String.format("%sms", end-start));
     }
}

结果是37040ms

计算hash后不可偏向

我们先来看看无锁状态计算hash后的对象布局

	public static void main(String[] args) throws Exception {
          A a = new A();
         System.out.println("jvm‐‐‐‐‐‐‐‐‐‐‐‐"+Integer.toHexString(a.hashCode()));
         System.out.println(ClassLayout.parseInstance(a).toPrintable());
     }

结果如下

jvm‐‐‐‐‐‐‐‐‐‐‐‐1540e19d
com.lry.basic.sync.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
      4     4        (object header)                           15 00 00 00 (00010101 00000000 00000000 00000000) (21)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (alignment/padding gap)                  
     16     8   long A.i                                       0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

对象状态是无锁状态
hashcode的16进制是1540e19d,从对象头第二行的VALUE第一个往前数正好是1540e19d.

启用偏向锁

	 public static void main(String[] args) throws Exception {
         Thread.sleep(5000);
          A a = new A();
//         System.out.println("jvm‐‐‐‐‐‐‐‐‐‐‐‐"+Integer.toHexString(a.hashCode()));
         synchronized (a){}
         System.out.println(ClassLayout.parseInstance(a).toPrintable());
     }

结果如下

com.lry.basic.sync.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 28 26 03 (00000101 00101000 00100110 00000011) (52832261)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4        (alignment/padding gap)                  
     16     8   long A.i                                       0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看出来确实是偏向锁,并且对象头还存储了偏向线程id。
但是把计算hash的代码打开后却发现是无锁状态,因为对象头要存储hashcode。

wait后变成重量级锁

JVM参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

	public static void main(String[] args) throws Exception {
        final A a = new A();
        System.out.println("before lock:"+ClassLayout.parseInstance(a).toPrintable());//偏,无偏向线程id
         Thread t = new Thread(){
             @Override
             public void run() {
                 synchronized (a){
                     System.out.println("before wait:"+ClassLayout.parseInstance(a).toPrintable());//偏,有偏向线程id
                     try {
                         a.wait();//释放锁a
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     System.out.println("after wait:"+ClassLayout.parseInstance(a).toPrintable());//重锁
                 }
             }
         };
        t.start();
        t.join(1000);//等待t线程执行完或者一秒后
        synchronized (a){
            a.notifyAll();
        }
        Thread.sleep(1000);//这里改成t.join() 发现竟然还是重锁,why?有人知道可以评论里面教下我
        System.out.println("after lock:"+ClassLayout.parseInstance(a).toPrintable());//无锁
        //批量偏向 阈值20
     }

运行结果和注释一样

批量重偏向

批量偏向阈值20
JVM参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
以class为单位,为每个class维护一个偏向锁撤销器,每当该class的对象发生偏向锁撤销操作,该计数器+1,当这个值到20,就会发生批量重偏向。

public static void main(String[] args) throws Exception {
        final List<A> list = new ArrayList<>();

        Thread t = new Thread(){
            @Override
            public void run() {
                for(int i=0;i<25;i++){
                    A a = new A();
                    synchronized (a){//偏向锁,偏心于t这个线程
                        list.add(a);
                    }
                }
            }
        };
        t.start();
        t.join();
        System.out.println("偏锁:"+ClassLayout.parseInstance(list.get(0)).toPrintable());//偏
        //<20 无竞争,线程交替执行 ,轻锁
        //>=20 批量偏向
        new Thread(){
            @Override
            public void run() {
                for(int i=0;i<list.size();i++){
                    A a = list.get(i);
                    synchronized (a){
                        if(i<19)//偏向锁撤销20次
                        System.out.println("轻锁:"+ClassLayout.parseInstance(a).toPrintable());
                        if(i>=19)
                        System.out.println("批量重偏向:"+ClassLayout.parseInstance(a).toPrintable());
                    }
                }
            }
        }.start();
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值