对象内存布局-JOL(Java Object Layout)
package pro.eddie.demo;
import org.openjdk.jol.info.ClassLayout;
public class JavaObjLayout {
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println(ClassLayout.parseInstance(demo).toPrintable());
}
private static class Demo {
private int i = 666;
private int j = 777;
}
}
运行结果:
可以发现对象布局分为4部分:
- MarkWord:记录了该对象的状态,有:无锁状态,加锁状态(偏向锁、自旋锁、重量锁),GC标记状态。
- class类型指针:通过该指针快速定位对应的Class类,getClass()方法则是通过该指针进行访问
- 实例数据:存放对象中的属性,若是基本数据类型则VALUE为对应的值,若是对象,则存放对象引用
- 对齐:64位操作系统下,对齐部分将会自动将对象内存补齐至能够被8整除的大小,既易于管理便于寻址,也避免产生碎片
偏向锁及其应用场景
偏向锁的实现是:Unlock状态下MarkWord的一个比特位用于标识该对象偏向锁是否被使用或者是否被禁止。如果该bit位为0,则该对象未被锁定,并且禁止偏向;如果该bit位为1,则意味着该对象已经在偏向锁开启状态,默认对象都是可偏向**匿名偏向(Anonymously biased)的,这是有一个线程来使用这个对象后,则可能转变为可重偏向(Rebiasable)或已偏向(Biased)状态;这时有其他线程再来访问该对象,通过判断持有此对象的线程,是否是正在使用此对象,若没有,则该对象是可重偏向(Rebiasable)**状态,通过CAS原子操作,来该对象的偏向锁绑定于线程自身。
适用场景:适用于单线程操作,线程数一多,偏向锁容易升级成轻量级锁,此时撤销偏向锁也需要耗费资源。
轻量级锁/自旋锁及其应用场景
适用场景:少量线程并发,且每个线程执行时间较短;原因:CAS操作依然占用着CPU的资源(取值、赋值、比较),若线程数量一多,线程执行时间一长,导致线程的自旋次数大大增多,得不偿失。
重量级锁及其应用场景
实现:每个Java对象与一个monitor
绑定关联,当一个线程想要执行一个Synchronized
代码块内的内容时,得先拿到对应对象的monitor
。并有以下规则:
- 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
- 若线程已拥有monitor的所有权,允许它重入monitor,并递增monitor的进入数
- 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权
- 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程,执行monitorexit时会将monitor的进入数减1。
- 当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权
HotSpot实现的Monitor
是由Cpp编写的ObjectMonitor
:
ObjectMonitor() {
_header = NULL;
_count = 0; //monitor进入数
_waiters = 0,
_recursions = 0; //线程的重入次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程
_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁进入时的单项链表
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
- cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接),cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程),因此_cxq是一个后进先出的stack(栈)。
- _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中
- _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中
适用场景:多线程并发,线程执行时间较长的情况下,在对象被锁定时,其他并发的线程处于阻塞状态,不会占用CPU资源。
锁升级的过程(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)
1、 无锁 -> 偏向锁:
默认情况下:`main`线程执行后,4秒延迟后才会开启偏向锁机制。
原因:JVM虚拟机自己有一些默认启动的线程,有不少的sync代码,这些代码启动时,就存在锁竞争,如果使用偏向锁,就会有许多锁撤销,锁升级的操作,使得效率降低。
2、 偏向锁 -> 轻量级锁:
程序启动4秒后,默认启动了偏向锁机制,当一个线程首次去访问`MarkWord`标记为偏向锁的对象(匿名偏向),则修改`MarkWord`指向该线程;第二个线程访问该对象时,发现该对象偏向锁被持有,则通过`MarkWord`上的线程信息去访问该线程,并判断该线程是否仍然需要持有偏向锁:不需要继续持有对象则通过CAS操作撤销该偏向锁,通过CAS操作修改`MarkWord`指向自身;需要继续持有对象的情况则通过CAS操作修改`MarkWord`锁类型信息,升级为轻量级锁。
3、 轻量级锁 -> 重量级锁:
自旋锁(轻量级锁)在JDK1.4.2引入,使用-XX:+UseSpinning来开启。
自旋超过10次,升级为重量级锁。
JDK6中变成默认开启,并引入了自适应的自旋锁(适应性自旋锁)。
适应性自旋锁:意味着自旋时间(次数)不再固定,而是由前一次在同一个锁上的自选时间及锁的拥有者状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。(简单来说就是:根据该锁上的成功的概率来决定是否要升级锁)