new出来对象在堆内存中的内存布局
- markword 8个字节 (synchronized主要影响的是markword)markword记录了锁信息,gc信息,hashcode
- klass pointer 指向类.class 即表示对象属于哪个class的对象 默认4个字节
- 成员变量,比如int m=8;int类型的变量占据4个字节
- padding对齐
64位的虚拟机,对齐是8个字节,之所以是8个字节是指整体这个对象所占用字节总和必须能被8整除
JOL=Java Object Layout工具的使用
jol是对象内存布局查看工具
引入
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
案例
情况一:下面我们用jol解析new出来的对象o的内存布局
public class JolTest {
public static void main(String[] args) {
Object o=new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
执行结果:
从第0个位置往后数4个字节和从第4个位置往后数4个字节总共8个字节这两部分加起来8个字节是markword,从第8个字节开始往后数4个是klass pointer,从第12个字节开始加4个字节对齐组成16个字节
情况二:下面我们对对象o加一把锁,然后来再看它的内存布局变化
public class JolTest {
public static void main(String[] args) {
Object o=new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
执行结果:
可以看到锁定对象前对象的markword和锁定对象后对象的markword不一样了,因此我们可以认识到,锁定对象后对象的markword中一定有锁信息
每个组的第一行的括号后面的第一个8位的最后3位可以看锁信息,不如上面是001(无锁态),下面是000(自旋锁)
左边是右边二进制位的十六进制表示形式,比如C8的二进制表示为11001000
总结
markword记录了锁信息,gc信息,hashcode
Synchronized锁升级过程
Synchronized锁在jdk1.2之前效率非常低,后面逐步优化,在jdk1.6以后优化到了一个比较好的状态,这个状态就是Synchronized锁升级过程
用户态和内核态
windows或者linux操作系统都有一个内核,称为kernel。最早的时候,kernel和我们的应用程序不区分,这就导致应用程序直接访问硬件(内存,网卡,显示器),很容易把硬件搞坏,比如直接把操作系统用的内存干掉,导致操作系统down
鉴于此,现在的操作系统一般分为两层,kernel工作的是内核态,我们自己的应用程序工作的时候是用户态。这样,如果用户态的程序要访问内存必须先经过内核态的允许,从用户态转到内核态,拿到结果以后再将数据从内核态转移到用户态
JVM相对于操作系统来说也就是一个普通程序。
JDK早期,Synchronized叫做重量级锁,因为申请锁必须通过内核kernel,系统调用才能拿到这把锁。之所以说是重就是因为需要经过操作系统内核的帮助,因此这是一个效率很低的耗时操作,即使线程很少的情况下
后面对锁逐步优化,就是某些情况下不再需要经过内核,直接在用户空间就可以解决,比如CAS,是轻量级锁
Synchronized锁升级过程
如果最后2位是0,1,则偏向锁和无锁态相同因此无法区分,所以再加一位,因此第三位叫偏向锁位
无锁态-> 偏向锁 ->轻量级锁(又称自旋锁/无锁(之所以叫原因是没有内核状态的锁即CAS))
偏向锁和自旋锁都是在用户空间完成的
重量级锁是需要向内核申请
偏向锁和偏向锁的升级
偏向锁默认是启动的,但是会延迟,JVM启动以后,4秒以后偏向锁才会起作用
就是偏向你一个人,偏向锁实际上就是没有锁,将自己的id贴上去即可,之所以会有偏向锁,因为本来很多时间就是只有一个线程在运行,但这个方法天生就加了一个synchronized,如果向内核申请一个重量级锁效率太低了。
比如我们业务白天访问量很大,存在并发访问情况,可能晚上就一个人访问或者没有,如果我们直接加个重量级锁很明显效率很低。
因此我们直接只是单纯加一个标记,比如张三要上厕所,他在门上贴一张条张三,然后就进去。如果这时候有其他两个线程也来抢这个资源的话,那么就要出现锁竞争,让张三暂停,然后将张三加的偏向锁的标记撤下来,即锁撤销(将张三的纸条撕下来),开始上自旋锁。因为偏向锁并未加锁,只是贴了个标记,不存在锁释放概念:张三醒过来,继续开始执行。其他线程等张三执行完后(之所以要等是因为如果其他线程抢占到了锁,那么张三这次操作就不具备原子性,是线程不安全的)释放锁后,谁能将自己的lock Record贴到门上去,谁就抢到了这把锁。当竞争越来越激烈的时候,才会升级为重量级锁
分析jvm源码(biasedLocking.cpp)解析的偏向锁升级流程,示例中:线程1当前拥有偏向锁对象,线程2是需要竞争到偏向锁。
线程2来竞争锁对象:
判断当前对象头是否是偏向锁;
判断拥有偏向锁的线程1是否还存在;
线程1不存在,直接设置偏向锁标识为0(线程1执行完毕后,不会主动去释放偏向锁);
使用cas替换偏向锁线程ID为线程2,锁不升级,仍为偏向锁;
线程1仍然存在,暂停线程1;
设置锁标志位为00(变为轻量级锁),偏向锁为0;
从线程1的空闲monitor record中读取一条,放至线程1的当前monitor record中;
更新mark word,将mark word指向线程1中monitor record的指针;
继续执行线程1的代码;
锁升级为轻量级锁;
线程2自旋来获取锁对象;
偏向锁升级轻量级锁条件:只要再来一个线程竞争就升级
偏向锁和自旋锁的区别:自旋锁是需要耗费cpu资源的
有了轻量级锁,为什么还要升级重量级锁
轻量级锁适合锁定以后执行时间较短的场景,或者线程数较少的场景。和重量级锁最重要的区别就是自旋锁是需要占用cpu资源的,因为自旋就要循环,而重量级锁是不需要消耗cpu资源的,原因是重量级锁下面有两个队列,一个竞争队列,一个等待队列。比如之前可能有100个线程在自旋消耗cpu资源,现在不需要再自旋,将其扔到一个等待队列waitSet中等着即可,什么时候轮到这个线程,操作系统将其叫醒然后执行,轮不到那个线程的时候,他就等着即可,属于阻塞
轻量级锁升级重量级锁条件:自旋超过10次,升级为重量级锁,原因:如果太多线程自旋CPU消耗过大,不然升级为重量级锁,进入等待队列(不消耗cpu)
总结
new一个对象,加锁的时候,先加上偏向锁,有轻度竞争的时候升级为轻量级锁,轻量级锁有重度竞争的话升级重量级锁(锁膨胀)。
如果有耗时过长的操作或者wait'操作,偏向锁会直接升级重量级锁
网友评论