发表于 2024/03/27 10:46:37 [java] 浏览次数:315
一、jvm-jmm
1、 JMM java内存模型
1)基本概念:
程序: 代码,指令序列。静态
进程: 程序的一次运行。动态,资源分配的基本单位。
线程: 一个进程可以包含多个线程,cpu调度的基本单位。
2)JVM与线程:
jvm什么时候启动?jvm也是软件,也是程序,运行在操作系统上,操作系统启动jvm执行java字节码。
3)JVM内存区域:
a.方法区: 用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区与永久代本质上并不等价,仅仅是因为HotSpot的设计团队选择把GC分代收集器扩展至方法区,或者说使用永久代来实现方法区,
这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,
能够省去专门为方法区编写内存管理代码的工作,对应i其他虚拟机是不存在永久代的概念
-XX:MaxPermSize :方法区的大小
已经发布的jdk1.7的HotSpot已经把放在永久代的字符串常量池移出(现在也有放弃永久代并逐步改为Native Memory来实现方法区的规划)
方法区特点:不需要连续的内存和可以选择固定大小或者可扩展 ;可以选择不实现垃圾回收
回收目标:针对常量池的回收和对类型的卸载
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
b.java堆: 此区域存放对象实例,几乎所有的对象实例都在这里分配。
从内存回收的角度:由于现在收集器基本都采用分代收集器,所以Java堆还可以细分为:新生代和老年代,再细致一点的有Eden空间、
From Survivor空间、To Survivor空间。
从内存分配的角度:线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer),
进一步划分的目的是为了更好的回收内存或者更快的分配内存。
c.程序计数器:看作是当前线程所执行字节码的行号指示器,此内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError情况的区域。
d.Java虚拟机栈: 虚拟机栈描述的是Java方法执行的内存模型, 每个方法在执行都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
OutOfMemoryError:如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存
e.本地方法栈: 与虚拟机栈的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务
f.运行时常量池:用于存放编译期生成的各种字面量和符号引用,这部分在类加载进入方法区的运行时常量池
运行时常量池对class文件的特征:1>保存class文件中描述的符号引用和翻译出来的直接引用 2>具备动态性,运行期间也可能将新的常量池放入池中
g.直接内存:不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。这部分内存也被频繁的使用,而且也可能导致 OutOfMemoryError异常
本机的直接内存不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)的大小以及处理器寻址空间的限制。
服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,
但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统的限制),从而导致动态扩展时出现OutOfMemoryError异常
JDK1.4加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,
避免了在Java堆和native堆中来回复制数据
4)JMM java内存模型:
抽象模型,规范。(可以认为jvm是对该规范的实现)
a.主内存 : 线程共享数据(可以与jvm的方法区和堆区对应)
b.工作内存: 线程的工作空间,线程私有;基本数据类型,直接分配到工作内存;引用的地址存放在工作内存,引用的对象存放在堆中。(可以与jvm的虚拟机栈对应,对应到硬件上包括寄存器和cpu缓存)
c.工作方式: A 线程修改私有数据,直接在工作空间修改
B 线程修改共享数据,把数据复制到工作空间中去,在工作空间中修改,修改完成以后,刷新内存中的数据
d.作用 :规范内存数据和工作空间数据的交互
5)cpu缓存一致性问题:
i.总线加锁:降低CPU的吞吐量
ii.缓存上的一致性协议(MESI):
当CPU在CACHE中操作数据时,如果该数据是共享变量,数据在CACHE读到寄存器中,进行新修改,并更新内存数据
CaCHE LINE置无效,其他的CPU就从内存中读数据。
6)并发编程的三个重要特性:
原子性:不可分割 x=1
可见性:线程只能操作自己工作空间中的数据
有序性:程序中的顺序不一定就是执行的顺序
编译重排序
指令重排序
提高效率
as-if-serial原则: 单线程中,重排后不影响执行结果。
happen-before:
单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用
7)JMM对三个特征的保证
1)JMM与原子性
A.X=10 写 原子性 如果是私有数据具有原子性,如果是共享数据没原子性(读写)
B.Y=x 没有原子性
a) 把数据X读到工作空间(原子性)
b) 把X的值写到Y(原子性)
C.I++ 没有原子性
a)读i到工作空间
b)+1;
c)刷新结果到内存
D.Z=z+1 没有原子性
a)读z到工作空间
b)+1;
c)刷新结果到内存
多个原子性的操作合并到一起没有原子性
保证方式:
Synchronized
JUC Lock的lock
2)JMM与可见性
Volatile:在JMM模型上实现MESI协议
Synchronized:加锁
JUC Lock
3)JMM与有序性
Volatile
Synchronized
Happens-before
2.synchronized
原子性,互斥性,可见性
根据获取的锁分类
1、获取对象锁
synchronized(this|object) {}
修饰非静态方法
在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
2、获取类锁
synchronized(类.class) {}
修饰静态方法
在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
在 Java 中,每个对象都会有一个 monitor 对象,监视器。
1)某一线程占有这个对象的时候,先monitor 的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;
如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1;
2)同一线程可以对同一对象进行多次加锁,+1,+1,重入性
分析命令: jconsole -- 可视化工具
jstack [pid] -- 查看线程状态
javap -v [xxx].class -- 反汇编
3)字节码反汇编分析
synchronized修饰代码块:
两个指令:
monitorenter -- 加锁
monitorexit -- 释放锁 (两个,一个正常出口,一个异常出口 都要释放锁)
synchronized修饰实例方法:
flags: ACC_PUBLIC, ACC_SYNCHRONIZED (标记说明是同步方法)
synchronized修饰静态方法
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED (标记说明是同步方法)
3、偏向锁、轻量级锁、重量级锁
对象:对象头,实例变量,填充数据
对象头:
MarkWord: 存对象的hashcode和锁信息
ClassMetadataAddress: 存指向对象类型的指针
ArrayLength: 数组长度(如果当前对象是数组)
无锁状态:没有加锁
偏向锁:在对象第一次被某一线程占有的时候,是否偏向锁(1个bit)置为1,锁标志位(2个bit)置为01,写入线程号,
当其他的线程访问的时候
和占有锁的线程是同一个线程->成功
否则竞争,失败 -> 轻量级锁
CAS算法 compare and set(CAS) 无锁状态时间非常接近 竞争不激烈的时候适用
轻量级锁:线程有交替执行(同一时刻只有一个线程会访问同步资源),互斥性不是很强,CAS失败,锁标志位00
重量级锁:强互斥,锁标志位10,等待时间长(依赖操作系统内核,用户态和内核态转换)
自旋锁:竞争失败的时候,不是马上转化级别,而是执行几次空循环
锁消除:JIT在编译的时候把不必要的锁去掉
锁会升级但不会降级。
3.volatile关键字
1)保证共享变量的可见性,当一个线程修一个共享变量时,另外一个线程能够读到这个修改的值。
2)保证有序性
重排序(编译阶段、指令优化阶段)
输入程序的代码顺序并不是实际执行的顺序
重排序后对单线程没有影响,对多线程有影响
Volatile
Happens-before
volatile规则:
对于volatile修饰的变量:
(1)volatile之前的代码不能调整到他的后面
(2)volatile之后的代码不能调整到他的前面(as if seria)
(3)霸道(位置不变化)
Int i=0;
Int a=3;
Int b=5;
Volatile Int j=3;
Int i=0;
Int a=3;
Int b=5;
Int m=i+j;
I++;
J++;
3)volatile的原理和实现机制(锁、轻量级)
HSDIS --反编译---汇编
Java --class---JVM---》ASM文件
Volatile int a ;
Lock :a
4)Volatile的使用场景:
a.状态标志(开关模式)
public class ShutDowsnDemmo extends Thread{
private volatile boolean started=false;
@Override
public void run() {
while(started){
dowork();
}
}
public void shutdown(){
started=false;
}
}
b.双重检查锁定(double-checked-locking)
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance == null){
instance=new Singleton();
}
}
}
return instance;
}
}
c.需要利用顺序性
5)volatile与synchronized的区别
a.Volatile只能修饰变量,synchronized只能修饰方法和语句块
b.synchronized可以保证原子性,Volatile不能保证原子性
c.都可以保证可见性,但实现原理不同
Volatile对变量加了lock,synchronized使用monitorEnter和monitorexit
d.对有序性的保证
Volatile能保证有序,synchronized可以保证有序性,但是代价(重量级)并发退化到串行
e.synchronized引起阻塞 Volatile不会引起阻塞
4.CAS
CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),
V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,
则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,
它总是认为自己可以成功完成操作。
CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,
然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令,
就是说CAS是靠硬件实现的,从而在硬件层面提升效率。
当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,
并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。
与锁相比,使用CAS会使程序看起来更加复杂一些,但由于其非阻塞的,它对死锁问题天生免疫,并且,线程间的相互影响也非常小。
更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,他要比基于锁的方式拥有更优越的性能。
简单的说,CAS 需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,哪说明它已经被别人修改过了。
你就需要重新读取,再次尝试修改就好了。
CAS底层原理
这样归功于硬件指令集的发展,实际上,我们可以使用同步将这两个操作变成原子的,但是这么做就没有意义了。所以我们只能靠硬件来完成,
硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。这类指令常用的有:
1. 测试并设置(Tetst-and-Set)
2. 获取并增加(Fetch-and-Increment)
3. 交换(Swap)
4. 比较并交换(Compare-and-Swap)
5. 加载链接/条件存储(Load-Linked/Store-Conditional)
CPU 实现原子指令有2种方式:
1. 通过总线锁定来保证原子性。
总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,
那么该处理器可以独占共享内存。但是该方法成本太大。因此有了下面的方式。
2、通过缓存锁定来保证原子性。
所谓 缓存锁定 是指内存区域如果被缓存在处理器的缓存行中,并且在Lock 操作期间被锁定,那么当他执行锁操作写回到内存时,处理器不在总线上声言 LOCK# 信号,
而时修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(这里和 volatile 的可见性原理相同),
当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
注意:有两种情况下处理器不会使用缓存锁定。
1. 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
2. 有些处理器不支持缓存锁定,对于 Intel 486 和 Pentium 处理器,就是锁定的内存区域在处理器的缓存行也会调用总线锁定。
CAS可以保证一次的读-改-写操作是原子操作,在单处理器上该操作容易实现,但是在多处理器上实现就有点儿复杂了。
缓存加锁:其实针对于上面那种情况我们只需要保证在同一时刻对某个内存地址的操作是原子性的即可。缓存加锁就是缓存在内存区域的数据如果在加锁期间,
当它执行锁操作写回内存时,处理器不在输出LOCK#信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,
也就是说当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行
CAS缺点:
CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。
循环时间太长:
如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。
只能保证一个共享变量原子操作:
看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。例如读写锁中state的高地位
ABA问题:
CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题
对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。