JVM

一.JVM体系结构概述

  1. **JVM是运行在操作系统之上的,它与硬件没有直接的交互**

灰色线程私有,内存占得少基本不存在垃圾回收 GC。
亮色线程共享。存在垃圾回收。

1.1ClassLoader

  1. 负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定<br />

1.2虚拟机自带的加载器

  1. 1.启动类加载器(BootstrapC++ 后台打印null 2.扩展类加载器(Extension Java 3.应用程序类加载器(AppClassLoader) Java 也叫系统类加载器,加载当前应用的classpath的所有类 sun.misc.Launcher$AppClassLoader@18b4aac2 <br /> 4.用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式<br />

1.3什么是双亲委派机制

  1. 当某个类加载器需要加载某个`.class`文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。<br /> 当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。 采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.0bject,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。<br />![](https://upload-images.jianshu.io/upload_images/7634245-7b7882e1f4ea5d7d.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp#align=left&display=inline&height=1166&margin=%5Bobject%20Object%5D&originHeight=1166&originWidth=1200&status=done&style=none&width=1200)

1.4双亲委派机制的作用

  1. 1、防止重复加载同一个`.class`。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 2、保证核心`.class`不能被篡改。通过委托方式,不会去篡改核心`.class`,即使篡改也不会去加载,即使加载也不会是同一个`.class`对象了。不同的加载器加载同一个`.class`也不是同一个`Class`对象。这样保证了`Class`执行安全。

1.5沙箱安全

  1. Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱**主要限制系统资源访问**,那系统资源包括什么?——`CPU、内存、文件系统、网络`。不同级别的沙箱对这些资源访问的限制也可以不一样。<br />找类先从最高层往下找 找不到就报classnotfound异常<br />xxxxxxxxxx<br />package java.lang;<br />public class String {<br /> public static void main(String[] args){<br /> System.out.println("*********");<br /> }<br />}

1.6Execution Engine

  1. Execution Engine执行引擎负责解释命令,提交操作系统执行。

2.1Native Interface本地接口

  1. native 方法代表java无法再执行,只能交由底层第三方库来执行。<br /> 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。<br /> 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。

2.2Native Method Stack

  1. 它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。

3.PC寄存器

  1. 根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。 这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。 如果执行的是一个Native方法,那这个计数器是空的。 用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误.

4.方法区

  1. 供各线程共享的运行时内存区域。**它存储了每一个类的结构信息**,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。 上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。 (不同版本的jdk有不同的方法区实现)<br />实例变量存在堆内存中,和方法区无关 不懂<br />xxxxxxxxxx<br />public class T2 {<br /> public static void main(String[] args) {<br /> <br /> }<br /> <br /> //实例变量跟随 对象存在堆中。<br /> public void say(){<br /> <br /> }<br />}

补充:

  1. Java66之前,常量池是存放在方法区(永久代)中的。<br /> Java7,将常量池是存放到了堆中。<br /> Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中。<br />[https://blog.csdn.net/terstdfhuc/article/details/86526047](https://blog.csdn.net/terstdfhuc/article/details/86526047)<br />栈:栈主要存放在运行期间用到的一些局部变量(基本数据类型的变量)或者是指向其他对象的一些引用,**因为方法执行时,被分配的内存就在栈中**,所以当然存储的局部变量就在栈中咯。当一段代码或者一个方法调用完毕后,栈中为这段代码所提供的基本数据类型或者对象的引用立即被释放。

5 stack

  1. **栈管运行堆管存储**

5.1 介绍

  1. 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。**8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配**。

5.2栈存储什么? 栈帧=java方法

栈帧中主要保存3类数据: 本地变量(Local Variables):输入参数和输出参数以及方法内的变量; 栈操作(Operand Stack):记录出栈、入栈的操作; 栈帧数据Frame Data):包括类文件、方法等等。

运行原理:
  1. 栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈,……执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧.遵循“先进后出”/“后进先出”原则。<br />xxxxxxxxxx<br /> <span style="color:red">每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息</span>,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。<span style="color:red">栈的大小和具体JVM的实现有关,通常在256K~756K之间,与等于1Mb左右</span>。 <br /> 每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕后会自动将此栈帧出栈。<br />xxxxxxxxxx<br />public static void main(String[] args) {<br /> m1();<br />}<br />public static void m1(){<br /> m1();<br />}<br />**Exception in thread "main" java.lang.StackOverflowError**<br />**jvm虚拟机 错误而非异常**

5.3栈+堆+方法区的交互关系


HotSpot是使用指针的方式来访问对象: Java堆中会存放访问类元数据的地址, reference存储的就直接是对象的地址
元数据:方法区中的运行时数据结构,存储了每一个类的结构信息 HotSpot:jdk8的名称

二.堆体系结构概述


一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
Young Generation Space 新生区 Young/NewTenure generation space 养老区 old/TenurePermanent Space 永久区(元空间) Perm

java7为永久代,java8更换为元空间 物理上只有新生+养老两部分

  1. 一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。<br />[https://www.jianshu.com/p/0192bcce7e0f(](https://www.jianshu.com/p/0192bcce7e0f()参考)

1.对象新建流程:

新建对象 在Eden
Eden 满了,开启 GC=YGC=轻GC Eden基本全部清空 正在使用的则复制保留到S0=from 继续新建继续YGC 将s0和Eden中保留下来的复制到S1=to中然后s1,s0交换from区和to区,他们的位置和名分不是固定的 每次GC都会交换,,GC之后有交换谁空谁是to
活过15(可修改)次后进去养老区OLD 满了,开启 Full GC=FGC Full GC 多次,发现养老区空间没法腾出来,则OOM 异常

2.新生区:

(如下是首次讲解,简单版,先入门大致理解,下一页ppt详细) 新生区是类的诞生、成长、消亡的区域,一个尖仕以里产生,问用,最后被垃圾回收器收集,结束生命。新生区乂分为两部分:可区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存О区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生0OM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用).

3.MinorGC的过程(复制->情况->交换)

1:eden、SurvivorFrom复制到SurvivorTo,年龄+1
  1. 首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorErom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1

2:清空eden.SurvivorErom
  1. 然后,清空EdenSurvivorFrom中的对象,也即复制之后有交换,谁空谁是to

3:Survivor To和SurvivorFrom互换
  1. 最后,SurvivorToSurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在FromTo区域中复制来复制去,如此**交换15次**(由VM参数MaxlenuringThreshold决定,**这个参数默认是15),最终如果还是存活,就存入到老年代**

4.永久代:

  1. 实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。<br /> 对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。jdk1.8 常量池在方法区中,字符串在堆中。
  2. 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

三.堆参数调优入门

3.1java7jdk


-Xms 初始化大小,,-Xmx最大值-Xmn 调整新生区大小。

3.2jdk8

3.3java8的元空间

  1. Java8中,永久代已经被移除,被一个称为元空间的区域所取代。在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。<br /> 元空间与永久代之间最大的区别在于: 永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。<br /> 因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

3.4堆内存调优简介

-Xms 设置初始分配大小,默认为物理内存的 1/64
-Xmx 最大分配内存,默认物理内存的 1/4
-XX:+PrintGCDetails 输出详细的GC处理日志
实际生产开发中初始化大小和最大内存分配是一样的,防止gc和应用程序争抢内存,造成峰值忽高忽低。

3.5配置堆内存参数优化


xxxxxxxxxx
public static void main(String[] args) {
long maxMemory = Runtime.getRuntime().maxMemory();
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println(“MAX_MEMEORY=”+maxMemory+(“字节”)+maxMemory/(double)1024/1024+”MB”);
System.out.println(“TOTAL_MEMEORY=”+totalMemory+(“字节”)+totalMemory/(double)1024/1024+”MB”);
}
xxxxxxxxxx
MAX_MEMEORY=1029177344字节981.5MB
TOTAL_MEMEORY=1029177344字节981.5MB
Heap
PSYoungGen total 305664K, used 20971K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3396K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 379K, capacity 388K, committed 512K, reserved 1048576K
3.6.OOM异常
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
xxxxxxxxxx
/ String str= “ww”;
while(true){
str+=str +new Random().nextInt(88888888)+new Random().nextInt(999999999);
}
/
byte[] bytes= new byte[4010241024];
xxxxxxxxxx
[GC (Allocation Failure)
[PSYoungGen: 2048K->488K(2560K)] 2048K->744K(9728K), 0.0012497 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure)
[PSYoungGen: 0K->0K(1536K)] 4334K->4334K(8704K), 0.0002491 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure)
[PSYoungGen: 0K->0K(1536K)]
[ParOldGen: 4334K->4314K(7168K)] 4334K->4314K(8704K),
[Metaspace: 3493K->3493K(1056768K)], 0.0054603 secs]
[Times: user=0.03 sys=0.00, real=0.01 secs]

四.GC是什么(分代收集算法)

4.1次数上频繁收集Young区

4.2次数上较少收集Old区

4.3基本不动元空间

五,GC算法

5.1GC算法概述

5.2 4算法

5.2.1引用计数法


xxxxxxxxxx
public class RefCountGC {
private byte[] bigSize= new byte[210241024]; //占空间
Object instane=null;
public static void main(String[] args) {
RefCountGC objectA = new RefCountGC();
RefCountGC objectB = new RefCountGC();
objectA.instane=objectB;
objectB.instane=objectA;
objectA=null;
objectB=null;
System.out.println(objectA.instane);
System.gc(); //开启后并不是立刻开启线程 稍等一会会
}
}
//循环引用 A B 互相指向对方 类似死锁,无法释放 几户没人使用

5.2.2复制算法(Copying)
  1. 年轻代中使用的是Minor GC,这种GC算法采用的是复制算法。<br /> Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Oldgeneration中,**也即一旦收集后,Eden是就变成空的了。**<br />当对象在Eden(包括一个Survivor区域,这里假设是from区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳(上面已经假设为from区域,这里应为to区域,即 to区域有足够的内存空间来存储Edenfrom区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另外一块Survivor区域(即to区域)中,然后清理所使用过的Eden以及 Survivor区域(即from区域),并且将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次MinorGC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,通过-XX:MaxTenuringThreshold来设定参数),这些对象就会成为老年代。<br /> Eden区满的时候,**会触发第一次young gc,**把还活着的对象拷贝到Survivor From区;**当Eden区再次触发young gc的时候,**会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将EdenFrom区域清空。 <br /> **当后续Eden又发生young gc的时候,**会对EdenTo区域进行垃圾回收,存活的对象复制到From区域,并将EdenTo区域清空。
  • 万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。

-XX:MaxTenuringThreshold —设置对象在新生代中存活的次数

缺点
  1. 1、它浪费了一半的内存,这太要命了。 2、如果对象的存活率很高,我们可以极端一点,假设是100%存活**,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间**,在对象存活率达到一定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费

5.2.3标记清除(Mark-Sweep)
  1. 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
  2. 用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行。

缺点
  1. 1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲 2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

5.2.4标记压缩(Mark-Compact) 整理算法 而非清除算法
  1. <br /> **在整理压缩阶段,不再对标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。**可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。<br /> 标记-压缩算法适用于存活对象比较多的场合,如老年代。它在标记-清除算法的基础上坐了一些优化。和标记-清除算法一样, 标记-压缩算法也是首先从根结点开始,对所有可以达到的对象进行一次标记。但之后,它并是要简单但清理未被标记的对象, 而是将所有存活的对象压缩到内存的一端。之后,清理所有边界外的空间。 **找出标记的对席后,开始移动存活对象,不是清除未标记的对象。移动完后 ,清理边界外的对象。**

缺点:
  1. 标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

标记清除压缩


这个也很好理解就是在整理阶段不再是GC一次就整理一次,而是每隔一段时间整理一次,减少移动对象的成本。

5.2.5算法比较
  1. 内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。 内存整齐度:复制算法=标记整理算法>标记清除算法。 内存利用率:标记整理算法=标记清除算法>复制算法。

年轻代(Young Gen)
  1. 年轻代特点是区域相对老年代较小,对像存活率低。<br /> 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关.内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。因而很适用于年轻代的回收。而复制算法

老年代(Tenure Gen)
  1. 老年代的特点是区域较大,对像存活率高。<br /> 这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。<br /> Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率。<br /> Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。<br /> **Compact阶段** 的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代的第一选择并不合适。<br /> 基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

六.JMM 内存模型

6.1JMM三个特点

  1. 可见性,原子性,有序性

原子性:(有疑问)
  1. 原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1

非原子性:

也就是整个过程中会出现线程调度器中断操作的现象
类似”a ++”这样的操作不具有原子性,因为它可能要经过以下两个步骤:
(1)取出 a 的值
(2)计算 a+1
如果有两个线程t1,t2在进行这样的操作。t1在第一步做完之后还没来得及加1操作就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1开始执行第二步(此时t1中a的值可能还是旧值,不是一定的,只有线程t2中a的值没有及时更新到t1中才会出现)。这个时候就出现了错误,t2的操作相当于被忽略了
类似于a += 1这样的操作都不具有原子性。还有一种特殊情况,就是long跟double类型某些情况也不具有原子性,具体可参考:java中long和double类型操作的非原子性探究
volatile是Java虚拟机提供的轻量级的同步机制

共享变量
  1. 如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这个几个线程的共享变量

可见性:
  1. 一个线程对共享变量值的修改,能够及时的被其它线程看到。 在多线程的情况下,共享变量不一定是可见的。要想实现变量的一定可见,可以使用synchronizedvolatile两种方式(其实还有final,但是它初始化后,值不可 更改,所以一般不用它实现可见性)。具体做法可参考:[synchronized实现可见性](http:_www.cnblogs.com_xuwenjin_p_9044230)、[volatile特性](http:_www.cnblogs.com_xuwenjin_p_9051179)

有序性:

指令重排。内存屏障

6.2JMM关于同步的规定:

  1. 1.线程解锁前,必须把共享变量的值刷新回主内存 2.线程加锁前,必须读取主内存的最新值到自己的工作内存 3.加锁解锁是同一把锁<br /> 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)须通过主内存来完成,其简要访问过程如下图:<br />

6.3JMM内存模型简介


  1) 所有的变量都存储在主内存中
  2)每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
用上面的图可以直观的看到JMM中,线程、工作内存、主内存之间的关系:
在上面的图中,可以看到线程只与工作内存交互,不能直接访问主内存。当主内存中有一个共享变量X,则工作内存则是将X拷贝,然后线程是操作工作内存中X的副本
在JMM中,有两条规定:
  1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
2)不同线程之间无法访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成 共享变量要实现可见性,必须经过如下两个步骤: 1)把工作内存1中更新过的共享变量刷新到主内存中 2)把主内存中最新的共享变量的值更新到工作内存2中
xxxxxxxxxx
public class JMMDemo {
public static void main(String[] args) {
MyNumber myNumber = new MyNumber();
new Thread(()->{
/try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/
myNumber.addTo1205();
System.out.println(Thread.currentThread().getName()+”\t Update number,value=”+myNumber.number);
},”AAA”).start();
while (myNumber.number==10)
{
//需要有一种通知机制告诉main线程,number已经修改为1205
//System.out.println(myNumber.number);
// System.out.println(count++); IO操作会抢夺cpu
//在这个循环中 不见得number就会一直为10而得不到通知变成1202
}
System.out.println(Thread.currentThread().getName()+”\t mission is over”);
}
}
class MyNumber{
volatile int number = 10;
public void addTo1205(){
this.number=1205;
}
}
volatile 当变量修改后,会通知其他线程来重新获取值

6.4JMM数据原子操作

  • read(读取)︰从主内存读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • use(使用)):从工作内存读取数据来计算
  • assign(赋值):将计算好的值重新赋值到工作内存中
  • store(存储):将工作内存数据写入主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定):将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
    CPU的缓存不一致问题(非JMM)
    总线加锁(性能太低)
    1. Cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其它cpu没法去读或写这个数据,直到这个cpu使用完数据释放锁之后其它cpu才能读取该数据
    MESI缓存一致性协议
    1. 据的变化从而将自己缓存里的数据失效多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数

    6.5Volatile

Volatile缓存可见性实现原理

底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存 IA-32架构软件开发者手册对lock指令的解释: 1.会将当前处理器缓存行的数据立即写回到系统内存。 2.这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI)
xxxxxxxxxx
// 可见性
public class VolatileVisible {
public static void main(String[] args) {
MyBoolean myBoolean = new MyBoolean();
new Thread(new Runnable() {
@Override
public void run() {
myBoolean.getTrue();
System.out.println(Thread.currentThread().getName()+”==========》”+myBoolean.flag);
}
}).start();
while (!myBoolean.flag){
//需要有一种通知机制告诉main线程,flag已经变化
// System.out.println(count++); IO操作会抢夺cpu
//在这个循环中 不见得flag就会一直为flag而得不到通知变成true
}
System.out.println(“mian is over”);
}
}
class MyBoolean{
boolean flag = false;
public void getTrue(){
flag=true;
}
}

Volatile不一定保证原子性

xxxxxxxxxx
//volatile 不能保证原子性
public class VolatileSeriaDemo {
// 定义一个int类型的遍历
public static void main(String[] args) {
VolatileAomicThreadDemo volatileAomicThread = new VolatileAomicThreadDemo();
for (int i=0;i<100;i++)
{
new Thread(volatileAomicThread).start();
}
}
}
class VolatileAomicThreadDemo implements Runnable{
private volatile int count = 0;
public void run(){
for(int x=0;x<100;x++){
count++;
System.out.println(“count====>>”+count);
}
}
}
xxxxxxxxxx
// 用原子性类 解决原子性问题
public class VolatileSeriaDemo2 {
// 定义一个int类型的遍历
public static void main(String[] args) {
VolatileAomicThreadDemo2 volatileAomicThread = new VolatileAomicThreadDemo2();
for (int i=0;i<100;i++)
{
new Thread(volatileAomicThread).start();
}
}
}
class VolatileAomicThreadDemo2 implements Runnable{
private AtomicInteger atomicInteger =new AtomicInteger(10);
public void run(){
for(int x=1;x<=100;x++){
System.out.println(“atomicInteger====>>”+atomicInteger.incrementAndGet());
}
}
}

Volatile禁止指令重排序

什么是重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
原因:一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种: 1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序; 2.指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序; 3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

xxxxxxxxxx
//指令重排代码
public class OutOFOrderDemo {
public static int a = 0,b=0;
public static int i = 0,j=0;
public static void main(String[] args) throws InterruptedException {
int count = 0;
while (true){
a=0;b=0;i=0;j=0;
count++;
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
a=1;
i=b;
}
});
Thread t2 =new Thread(new Runnable() {
@Override
public void run() {
b=1;
j=a;
}
});
//得到结果
t1.start();
t2.start();
t1.join(); // t1优先执行完毕
t2.join();
System.out.println(“count=”+count+”===>>>>”+”i=”+i+”j=”+j);
if (i==0&&j==0){
break;
}
}
}
}