Java内存模型
JVM三部分:
1.类加载子系统
2.内存空间(运行时数据空间)
3.执行引擎
JVM运行时数据区
image.pngXXX.java ---->编译后:XXX.class ---->类装载子系统--->装载至JVM运行时数据区(内存)--->执行引擎执行
由所有线程共享的数据区:方法区,堆
线程隔离数据区:java栈,本地方法栈,程序计数器
数据结构:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
image.png堆,垃圾回收GC方式 针对full GC(zgc,G1,cms)
image.png
内存可见性、重排序、顺序一致性、volatile、锁、final
Valatile 实现及应用
volatile关键字,在转为汇编语言后,会发现有lock前缀的指令
0x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance
; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)
lock前缀指令会强制处理器将修改内容回写到内存,同时,通过缓存一致性协议(MESI)来控制缓存一致
即,一个处理器的缓存回写到内存会导致其他处理器的缓存无效
MESI: modified(修改),exclusive(独占),share(共享),Invalid(无效)
具体步骤及结果参考:http://www.cnblogs.com/xrq730/p/7048693.html
synchronized 实现及原理
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
1. 普通同步方法,锁是当前实例对象
2. 静态同步方法,锁是当前类的class对象
3. 同步方法块,锁是括号里面的对象
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的
如下
public class SynchronizedTest {
public synchronized void test1(){
}
public void test2(){
synchronized (this){
}
}
}
利用javap工具查看生成的class文件信息来分析Synchronize的实现
image.png
Java对象头,这对深入理解synchronized实现原理非常关键。
所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
理解Java对象头与Monitor
本段摘自:https://blog.csdn.net/javazejian/article/details/72828483
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:
image.png-
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
-
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
32/64bit | Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|
无锁状态 | 对象HashCode | 对象分代年龄 | 0 | 01 |
由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:
其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
image.png由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁
Java虚拟机对synchronized的优化
锁的四种状态:
无锁状态
偏向锁状态:只有一个线程进入临界区(加锁解锁不需要额外消耗,直接比较对象头里当前持有的线程ID,但是如果存在锁竞争会有额外的所撤销消耗)
轻量级锁状态:多个线程先后(交替)进入临界区(线程竞争使用自旋,不会阻塞,但是始终得不到锁会消耗CPU)
重量级锁状态:多个线程同时进入临界区(线程竞争不使用自旋,不会消耗CPU,但是线程阻塞,相应时间缓慢)
锁升级触发条件:
处于偏向锁状态时,有其他线程发起锁竞争,原持有锁线程未退出同步代码块——>升级为轻量级锁状态
处于轻量级锁状态时,其他线程多次自旋,cas操作仍然失败,未获取到锁——>升级为重量级锁状态
处于重量级锁状态时,线程会阻塞,等待原持有锁线程完成后退出再被唤醒
参考:
https://www.zhihu.com/question/53826114
https://blog.csdn.net/javazejian/article/details/72828483