【JVM】关于JVM,你须要知道这些!!

写在前面

最近,一直有小伙伴让我整理下关于JVM的知识,通过十几天的收集与整理,第一版算是整理出来了。但愿对你们有所帮助。java

JDK 是什么?

JDK 是用于支持 Java 程序开发的最小环境。程序员

  1. Java 程序设计语言
  2. Java 虚拟机
  3. Java API类库

JRE 是什么?

JRE 是支持 Java 程序运行的标准环境。算法

  1. Java SE API 子集
  2. Java 虚拟机

Java历史版本的特性?

Java Version SE 5.0

  • 引入泛型;
  • 加强循环,可使用迭代方式;
  • 自动装箱与自动拆箱;
  • 类型安全的枚举;
  • 可变参数;
  • 静态引入;
  • 元数据(注解);
  • 引入Instrumentation。

Java Version SE 6

  • 支持脚本语言;
  • 引入JDBC 4.0 API;
  • 引入Java Compiler API;
  • 可插拔注解;
  • 增长对Native PKI(Public Key Infrastructure)、Java GSS(Generic Security Service)、Kerberos和LDAP(Lightweight Directory Access Protocol)的支持;
  • 继承Web Services;
  • 作了不少优化。

Java Version SE 7

  • switch语句块中容许以字符串做为分支条件;
  • 在建立泛型对象时应用类型推断;
  • 在一个语句块中捕获多种异常;
  • 支持动态语言;
  • 支持try-with-resources;
  • 引入Java NIO.2开发包;
  • 数值类型能够用2进制字符串表示,而且能够在字符串表示中添加下划线;
  • 钻石型语法;
  • null值的自动处理。

Java 8

  • 函数式接口
  • Lambda表达式
  • Stream API
  • 接口的加强
  • 时间日期加强API
  • 重复注解与类型注解
  • 默认方法与静态方法
  • Optional 容器类

运行时数据区域包括哪些?

  1. 程序计数器
  2. Java 虚拟机栈
  3. 本地方法栈
  4. Java 堆
  5. 方法区
  6. 运行时常量池
  7. 直接内存

程序计数器(线程私有)

程序计数器(Program Counter Register)是一块较小的内存空间,能够看做是当前线程所执行字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器完成。编程

因为 Java 虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换后能恢复到正确的执行位置,每条线程都须要一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。数组

  1. 若是线程正在执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址;
  2. 若是正在执行的是 Native 方法,这个计数器的值为空。

程序计数器是惟一一个没有规定任何 OutOfMemoryError 的区域。缓存

Java 虚拟机栈(线程私有)

Java 虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期与线程相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每一个方法被执行的时候都会建立一个栈帧(Stack Frame),存储安全

  1. 局部变量表
  2. 操做栈
  3. 动态连接
  4. 方法出口

每个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。微信

这个区域有两种异常状况:数据结构

  1. StackOverflowError:线程请求的栈深度大于虚拟机所容许的深度
  2. OutOfMemoryError:虚拟机栈扩展到没法申请足够的内存时

本地方法栈(线程私有)

虚拟机栈为虚拟机执行 Java 方法(字节码)服务。多线程

本地方法栈(Native Method Stacks)为虚拟机使用到的 Native 方法服务。

Java 堆(线程共享)

Java 堆(Java Heap)是 Java 虚拟机中内存最大的一块。Java 堆在虚拟机启动时建立,被全部线程共享。

做用:存放对象实例。垃圾收集器主要管理的就是 Java 堆。Java 堆在物理上能够不连续,只要逻辑上连续便可。

方法区(线程共享)

方法区(Method Area)被全部线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

和 Java 堆同样,不须要连续的内存,能够选择固定的大小,更能够选择不实现垃圾收集。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。保存 Class 文件中的符号引用、翻译出来的直接引用。运行时常量池能够在运行期间将新的常量放入池中。

Java 中对象访问是如何进行的?

Object obj =  new  Object();

对于上述最简单的访问,也会涉及到 Java 栈、Java 堆、方法区这三个最重要内存区域。

Object obj

若是出如今方法体中,则上述代码会反映到 Java 栈的本地变量表中,做为 reference 类型数据出现。

new  Object()

反映到 Java 堆中,造成一块存储了 Object 类型全部对象实例数据值的内存。Java堆中还包含对象类型数据的地址信息,这些类型数据存储在方法区中。

如何判断对象是否“死去”?

  1. 引用计数法
  2. 根搜索算法

什么是引用计数法?

给对象添加一个引用计数器,每当有一个地方引用它,计数器就+1,;当引用失效时,计数器就-1;任什么时候刻计数器都为0的对象就是不能再被使用的。

引用计数法的缺点?

很难解决对象之间的循环引用问题。

什么是根搜索算法?

经过一系列的名为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来讲就是从 GC Roots 到这个对象不可达)时,则证实此对象是不可用的。

在这里插入图片描述

Java 的4种引用方式?

在 JDK 1.2 以后,Java 对引用的概念进行了扩充,将引用分为

  1. 强引用 Strong Reference
  2. 软引用 Soft Reference
  3. 弱引用 Weak Reference
  4. 虚引用 Phantom Reference

强引用

Object obj =  new  Object();

代码中广泛存在的,像上述的引用。只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

软引用

用来描述一些还有用,但并不是必须的对象。软引用所关联的对象,有在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围,并进行第二次回收。若是此次回收仍是没有足够的内存,才会抛出内存异常。提供了 SoftReference 类实现软引用。

弱引用

描述非必须的对象,强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一次垃圾收集发生前。当垃圾收集器工做时,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。提供了 WeakReference 类来实现弱引用。

虚引用

一个对象是否有虚引用,彻底不会对其生存时间够成影响,也没法经过虚引用来取得一个对象实例。为一个对象关联虚引用的惟一目的,就是但愿在这个对象被收集器回收时,收到一个系统通知。提供了 PhantomReference 类来实现虚引用。

有哪些垃圾收集算法?

  1. 标记-清除算法
  2. 复制算法
  3. 标记-整理算法
  4. 分代收集算法

标记-清除算法(Mark-Sweep)

什么是标记-清除算法?

分为标记和清除两个阶段。首先标记出全部须要回收的对象,在标记完成后统一回收被标记的对象。

有什么缺点?

效率问题:标记和清除过程的效率都不高。

空间问题:标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能致使,程序分配较大对象时没法找到足够的连续内存,不得不提早出发另外一次垃圾收集动做。

在这里插入图片描述

复制算法(Copying)- 新生代

将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另外一块上面,而后再把已经使用过的内存空间一次清理掉。

优势

复制算法使得每次都是针对其中的一块进行内存回收,内存分配时也不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。

缺点

将内存缩小为原来的一半。在对象存活率较高时,须要执行较多的复制操做,效率会变低。

在这里插入图片描述

应用

商业的虚拟机都采用复制算法来回收新生代。由于新生代中的对象容易死亡,因此并不须要按照1:1的比例划份内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。每次使用 Eden 和其中的一块 Survivor。

当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。Hotspot 虚拟机默认 Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80% + 10%),只有10%的内存是会被“浪费”的。

标记-整理算法(Mark-Compact)-老年代

标记过程仍然与“标记-清除”算法同样,但不是直接对可回收对象进行清理,而是让全部存活的对象向一端移动,而后直接清理掉边界之外的内存。

在这里插入图片描述

分代收集算法

根据对象的存活周期,将内存划分为几块。通常是把 Java 堆分为新生代和老年代,这样就能够根据各个年代的特色,采用最适当的收集算法。

  • 新生代:每次垃圾收集时会有大批对象死去,只有少许存活,因此选择复制算法,只须要少许存活对象的复制成本就能够完成收集。
  • 老年代:对象存活率高、没有额外空间对它进行分配担保,必须使用“标记-清理”或“标记-整理”算法进行回收。

Minor GC 和 Full GC有什么区别?

Minor GC:新生代 GC,指发生在新生代的垃圾收集动做,由于 Java 对象大多死亡频繁,因此 Minor GC 很是频繁,通常回收速度较快。
Full GC:老年代 GC,也叫 Major GC,速度通常比 Minor GC 慢 10 倍以上。

Java 内存

为何要将堆内存分区?

对于一个大型的系统,当建立的对象及方法变量比较多时,即堆内存中的对象比较多,若是逐一分析对象是否该回收,效率很低。分区是为了进行模块化管理,管理不一样的对象及变量,以提升 JVM 的执行效率。

堆内存分为哪几块?

  1. Young Generation Space 新生区(也称新生代)
  2. Tenure Generation Space养老区(也称旧生代)
  3. Permanent Space 永久存储区

分代收集算法

内存分配有哪些原则?

  1. 对象优先分配在 Eden
  2. 大对象直接进入老年代
  3. 长期存活的对象将进入老年代
  4. 动态对象年龄断定
  5. 空间分配担保

Young Generation Space (采用复制算法)

主要用来存储新建立的对象,内存较小,垃圾回收频繁。这个区又分为三个区域:一个 Eden Space 和两个 Survivor Space。

  • 当对象在堆建立时,将进入年轻代的Eden Space。
  • 垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,若是对象仍然存活,则复制到B Suvivor Space,若是B Suvivor Space已经满,则复制 Old Gen
  • 扫描A Suvivor Space时,若是对象已经通过了几回的扫描仍然存活,JVM认为其为一个Old对象,则将其移到Old Gen。
  • 扫描完毕后,JVM将Eden Space和A Suvivor Space清空,而后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。

Tenure Generation Space(采用标记-整理算法)

主要用来存储长时间被引用的对象。它里面存放的是通过几回在 Young Generation Space 进行扫描判断过仍存活的对象,内存较大,垃圾回收频率较小。

Permanent Space

存储不变的类定义、字节码和常量等。

Class文件

Java虚拟机的平台无关性

在这里插入图片描述

Class文件的组成?

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目间没有任何分隔符。当遇到8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。

魔数与Class文件的版本

每一个Class文件的头4个字节称为魔数(Magic Number),它的惟一做用是用于肯定这个文件是否为一个能被虚拟机接受的Class文件。OxCAFEBABE。

接下来是Class文件的版本号:第5,6字节是次版本号(Minor Version),第7,8字节是主版本号(Major Version)。

使用JDK 1.7编译输出Class文件,格式代码为:

在这里插入图片描述

前四个字节为魔数,次版本号是0x0000,主版本号是0x0033,说明本文件是能够被1.7及以上版本的虚拟机执行的文件。

  • 33:JDK1.7
  • 32:JDK1.6
  • 31:JDK1.5
  • 30:JDK1.4
  • 2F:JDK1.3

在这里插入图片描述

类加载器

类加载器的做用是什么?

类加载器实现类的加载动做,同时用于肯定一个类。对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在Java虚拟机中的惟一性。即便两个类来源于同一个Class文件,只要加载它们的类加载器不一样,这两个类就不相等。

类加载器有哪些?

  1. 启动类加载器(Bootstrap ClassLoader):使用C++实现(仅限于HotSpot),是虚拟机自身的一部分。负责将存放在\lib目录中的类库加载到虚拟机中。其没法被Java程序直接引用。
  2. 扩展类加载器(Extention ClassLoader)由ExtClassLoader实现,负责加载\lib\ext目录中的全部类库,开发者能够直接使用。
  3. 应用程序类加载器(Application ClassLoader):由APPClassLoader实现。负责加载用户类路径(ClassPath)上所指定的类库。

类加载机制

什么是双亲委派模型?

双亲委派模型(Parents Delegation Model)要求除了顶层的启动类加载器外,其他加载器都应当有本身的父类加载器。类加载器之间的父子关系,经过组合关系复用。
工做过程:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器完成。每一个层次的类加载器都是如此,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有到父加载器反馈本身没法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试本身去加载。

为何要使用双亲委派模型,组织类加载器之间的关系?

Java类随着它的类加载器一块儿具有了一种带优先级的层次关系。好比java.lang.Object,它存放在rt.jar中,不管哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,所以Object类在程序的各个类加载器环境中,都是同一个类。

若是没有使用双亲委派模型,让各个类加载器本身去加载,那么Java类型体系中最基础的行为也得不到保障,应用程序会变得一片混乱。

在这里插入图片描述

什么是类加载机制?

Class文件描述的各类信息,都须要加载到虚拟机后才能运行。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

虚拟机和物理机的区别是什么?

这两种机器都有代码执行的能力,可是:

  • 物理机的执行引擎是直接创建在处理器、硬件、指令集和操做系统层面的。
  • 虚拟机的执行引擎是本身实现的,所以能够自行制定指令集和执行引擎的结构体系,而且可以执行那些不被硬件直接支持的指令集格式。

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构, 存储了方法的

  • 局部变量表
  • 操做数栈
  • 动态链接
  • 方法返回地址

每个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在这里插入图片描述

Java 方法调用

什么是方法调用?

方法调用惟一的任务是肯定被调用方法的版本(调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

Java的方法调用,有什么特殊之处?

Class文件的编译过程不包含传统编译的链接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。这使得Java有强大的动态扩展能力,但使Java方法的调用过程变得相对复杂,须要在类加载期间甚至到运行时才能肯定目标方法的直接引用。

Java虚拟机调用字节码指令有哪些?

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器方法、私有方法和父类方法
  • invokevirtual:调用全部的虚方法
  • invokeinterface:调用接口方法

虚拟机是如何执行方法里面的字节码指令的?

解释执行(经过解释器执行)
编译执行(经过即时编译器产生本地代码)

解释执行

当主流的虚拟机中都包含了即时编译器后,Class文件中的代码到底会被解释执行仍是编译执行,只有虚拟机本身才能准确判断。

Javac编译器完成了程序代码通过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。由于这一动做是在Java虚拟机以外进行的,而解释器在虚拟机的内部,因此Java程序的编译是半独立的实现。

基于栈的指令集和基于寄存器的指令集

什么是基于栈的指令集?

Java编译器输出的指令流,里面的指令大部分都是零地址指令,它们依赖操做数栈进行工做。

计算“1+1=2”,基于栈的指令集是这样的:

iconst_1
iconst_1
iadd
istore_0

两条iconst_1指令连续地把两个常量1压入栈中,iadd指令把栈顶的两个值出栈相加,把结果放回栈顶,最后istore_0把栈顶的值放到局部变量表的第0个Slot中。

什么是基于寄存器的指令集?

最典型的是x86的地址指令集,依赖寄存器工做。
计算“1+1=2”,基于寄存器的指令集是这样的:

mov eax,  1
add eax,  1

mov指令把EAX寄存器的值设为1,而后add指令再把这个值加1,结果就保存在EAX寄存器里。

基于栈的指令集的优缺点?

优势:

  • 可移植性好:用户程序不会直接用到这些寄存器,由虚拟机自行决定把一些访问最频繁的数据(程序计数器、栈顶缓存)放到寄存器以获取更好的性能。
  • 代码相对紧凑:字节码中每一个字节就对应一条指令
  • 编译器实现简单:不须要考虑空间分配问题,所需空间都在栈上操做

缺点:

  • 执行速度稍慢
  • 完成相同功能所需的指令熟练多

频繁的访问栈,意味着频繁的访问内存,相对于处理器,内存才是执行速度的瓶颈。

Javac编译过程分为哪些步骤?

  1. 解析与填充符号表
  2. 插入式注解处理器的注解处理
  3. 分析与字节码生成
    在这里插入图片描述

什么是即时编译器?

Java程序最初是经过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code)。

为了提升热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各类层次的优化,完成这个任务的编译器成为即时编译器(Just In Time Compiler,JIT编译器)。

解释器和编译器

许多主流的商用虚拟机,都同时包含解释器和编译器。

  • 当程序须要快速启动和执行时,解释器首先发挥做用,省去编译的时间,当即执行。
  • 当程序运行后,随着时间的推移,编译器逐渐发挥做用,把愈来愈多的代码编译成本地代码,能够提升执行效率。

若是内存资源限制较大(部分嵌入式系统),可使用解释执行节约内存,反之可使用编译执行来提高效率。同时编译器的代码还能退回成解释器的代码。

在这里插入图片描述

为何要采用分层编译?

由于即时编译器编译本地代码须要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间越长。

分层编译器有哪些层次?

分层编译根据编译器编译、优化的规模和耗时,划分不一样的编译层次,包括:

  • 第0层:程序解释执行,解释器不开启性能监控功能,可出发第1层编译。
  • 第1层:也成为C1编译,将字节码编译为本地代码,进行简单可靠的优化,若有必要加入性能监控的逻辑。
  • 第2层:也成为C2编译,也是将字节码编译为本地代码,可是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

用Client Compiler和Server Compiler将会同时工做。用Client Compiler获取更高的编译速度,用Server Compiler获取更好的编译质量。

编译对象与触发条件

热点代码有哪些?

  • 被屡次调用的方法
  • 被屡次执行的循环体

如何判断一段代码是否是热点代码?

要知道一段代码是否是热点代码,是否是须要触发即时编译,这个行为称为热点探测。主要有两种方法:

  • 基于采样的热点探测,虚拟机周期性检查各个线程的栈顶,若是发现某个方法常常出如今栈顶,那这个方法就是“热点方法”。实现简单高效,可是很难精确确认一个方法的热度。
  • 基于计数器的热点探测,虚拟机会为每一个方法创建计数器,统计方法的执行次数,若是执行次数超过必定的阈值,就认为它是热点方法。

HotSpot虚拟机使用第二种,有两个计数器:

  • 方法调用计数器
  • 回边计数器(判断循环代码)

方法调用计数器统计方法

统计的是一个相对的执行频率,即一段时间内方法被调用的次数。当超过必定的时间限度,若是方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减小一半,这个过程称为方法调用计数器的热度衰减,这个时间就被称为半衰周期。

有哪些经典的优化技术(即时编译器)?

  • 语言无关的经典优化技术之一:公共子表达式消除
  • 语言相关的经典优化技术之一:数组范围检查消除
  • 最重要的优化技术之一:方法内联
  • 最前沿的优化技术之一:逃逸分析

公共子表达式消除

广泛应用于各类编译器的经典优化技术,它的含义是:

若是一个表达式E已经被计算过了,而且从先前的计算到如今E中全部变量的值都没有发生变化,那么E的此次出现就成了公共子表达式。没有必要从新计算,直接用结果代替E就能够了。

数组边界检查消除

由于Java会自动检查数组越界,每次数组元素的读写都带有一次隐含的条件断定操做,对于拥有大量数组访问的程序代码,这无疑是一种性能负担。

若是数组访问发生在循环之中,而且使用循环变量来进行数组访问,若是编译器只要经过数据流分析就能够断定循环变量的取值范围永远在数组区间内,那么整个循环中就能够把数组的上下界检查消除掉,能够节省不少次的条件判断操做。

方法内联

内联消除了方法调用的成本,还为其余优化手段创建良好的基础。

编译器在进行内联时,若是是非虚方法,那么直接内联。若是遇到虚方法,则会查询当前程序下是否有多个目标版本可供选择,若是查询结果只有一个版本,那么也能够内联,不过这种内联属于激进优化,须要预留一个逃生门(Guard条件不成立时的Slow Path),称为守护内联。

若是程序的后续执行过程当中,虚拟机一直没有加载到会令这个方法的接受者的继承关系发现变化的类,那么内联优化的代码能够一直使用。不然须要抛弃掉已经编译的代码,退回到解释状态执行,或者从新进行编译。

逃逸分析

逃逸分析的基本行为就是分析对象动态做用域:当一个对象在方法里面被定义后,它可能被外部方法所引用,这种行为被称为方法逃逸。被外部线程访问到,被称为线程逃逸。

若是对象不会逃逸到方法或线程外,能够作什么优化?

  • 栈上分配:通常对象都是分配在Java堆中的,对于各个线程都是共享和可见的,只要持有这个对象的引用,就能够访问堆中存储的对象数据。可是垃圾回收和整理都会耗时,若是一个对象不会逃逸出方法,可让这个对象在栈上分配内存,对象所占用的内存空间就能够随着栈帧出栈而销毁。若是能使用栈上分配,那大量的对象会随着方法的结束而自动销毁,垃圾回收的压力会小不少。
  • 同步消除:线程同步自己就是很耗时的过程。若是逃逸分析能肯定一个变量不会逃逸出线程,那这个变量的读写确定就不会有竞争,同步措施就能够消除掉。
  • 标量替换:不建立这个对象,直接建立它的若干个被这个方法使用到的成员变量来替换。

Java与C/C++的编译器对比

  1. 即时编译器运行占用的是用户程序的运行时间,具备很大的时间压力。
  2. Java语言虽然没有virtual关键字,可是使用虚方法的频率远大于C++,因此即时编译器进行优化时难度要远远大于C++的静态优化编译器。
  3. Java语言是能够动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,使得全局的优化难以进行,由于编译器没法看见程序的全貌,编译器不得不时刻注意并随着类型的变化,而在运行时撤销或从新进行一些优化。
  4. Java语言对象的内存分配是在堆上,只有方法的局部变量才能在栈上分配。C++的对象有多种内存分配方式。

物理机如何处理并发问题?

运算任务,除了须要处理器计算以外,还须要与内存交互,如读取运算数据、存储运算结果等(不能仅靠寄存器来解决)。
计算机的存储设备和处理器的运算速度差了几个数量级,因此不得不加入一层读写速度尽量接近处理器运算速度的高速缓存(Cache),做为内存与处理器之间的缓冲:将运算须要的数据复制到缓存中,让运算快速运行。当运算结束后再从缓存同步回内存,这样处理器就无需等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,可是引入了一个新的问题:缓存一致性。在多处理器系统中,每一个处理器都有本身的高速缓存,它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存时,可能致使各自的缓存数据不一致。
为了解决一致性的问题,须要各个处理器访问缓存时遵循缓存一致性协议。同时为了使得处理器充分被利用,处理器可能会对输出代码进行乱序执行优化。Java虚拟机的即时编译器也有相似的指令重排序优化。

Java 内存模型

什么是Java内存模型?

Java虚拟机的规范,用来屏蔽掉各类硬件和操做系统的内存访问差别,以实现让Java程序在各个平台下都能达到一致的并发效果。

Java内存模型的目标?

定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,可是不包括局部变量和方法参数,由于这些是线程私有的,不会被共享,因此不存在竞争问题。

主内存与工做内存

因此的变量都存储在主内存,每条线程还有本身的工做内存,保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的全部操做(读取、赋值)都必须在工做内存中进行,不能直接读写主内存的变量。不一样的线程之间也没法直接访问对方工做内存的变量,线程间变量值的传递须要经过主内存。

在这里插入图片描述

内存间的交互操做

一个变量如何从主内存拷贝到工做内存、如何从工做内存同步回主内存,Java内存模型定义了8种操做:

在这里插入图片描述

原子性、可见性、有序性

  • 原子性:对基本数据类型的访问和读写是具有原子性的。对于更大范围的原子性保证,可使用字节码指令monitorenter和monitorexit来隐式使用lock和unlock操做。这两个字节码指令反映到Java代码中就是同步块——synchronized关键字。所以synchronized块之间的操做也具备原子性。
  • 可见性:当一个线程修改了共享变量的值,其余线程可以当即得知这个修改。Java内存模型是经过在变量修改后将新值同步回主内存,在变量读取以前从主内存刷新变量值来实现可见性的。volatile的特殊规则保证了新值可以当即同步到主内存,每次使用前当即从主内存刷新。synchronized和final也能实现可见性。final修饰的字段在构造器中一旦被初始化完成,而且构造器没有把this的引用传递出去,那么其余线程中就能看见final字段的值。
  • 有序性:Java程序的有序性能够总结为一句话,若是在本线程内观察,全部的操做都是有序的(线程内表现为串行的语义);若是在一个线程中观察另外一个线程,全部的操做都是无序的(指令重排序和工做内存与主内存同步延迟线性)。

volatile

什么是volatile?

关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile以后,具有两种特性:

  1. 保证此变量对全部线程的可见性。当一条线程修改了这个变量的值,新值对于其余线程是能够当即得知的。而普通变量作不到这一点。
  2. 禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程当中,获得正确结果,可是不保证程序代码的执行顺序。

为何基于volatile变量的运算在并发下不必定是安全的?

volatile变量在各个线程的工做内存,不存在一致性问题(各个线程的工做内存中volatile变量,每次使用前都要刷新到主内存)。可是Java里面的运算并不是原子操做,致使volatile变量的运算在并发下同样是不安全的。

为何使用volatile?

在某些状况下,volatile同步机制的性能要优于锁(synchronized关键字),可是因为虚拟机对锁实行的许多消除和优化,因此并非很快。

volatile变量读操做的性能消耗与普通变量几乎没有差异,可是写操做则可能慢一些,由于它须要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

并发与线程

并发与线程的关系?

并发不必定要依赖多线程,PHP中有多进程并发。可是Java里面的并发是多线程的。

什么是线程?

线程是比进程更轻量级的调度执行单位。线程能够把一个进程的资源分配和执行调度分开,各个线程既能够共享进程资源(内存地址、文件I/O),又能够独立调度(线程是CPU调度的最基本单位)。

实现线程有哪些方式?

  • 使用内核线程实现
  • 使用用户线程实现
  • 使用用户线程+轻量级进程混合实现

Java线程的实现

操做系统支持怎样的线程模型,在很大程度上就决定了Java虚拟机的线程是怎样映射的。

Java线程调度

什么是线程调度?

线程调度是系统为线程分配处理器使用权的过程。

线程调度有哪些方法?

  • 协同式线程调度:实现简单,没有线程同步的问题。可是线程执行时间不可控,容易系统崩溃。
  • 抢占式线程调度:每一个线程由系统来分配执行时间,不会有线程致使整个进程阻塞的问题。

虽然Java线程调度是系统自动完成的,可是咱们能够建议系统给某些线程多分配点时间——设置线程优先级。Java语言有10个级别的线程优先级,优先级越高的线程,越容易被系统选择执行。

可是并不能彻底依靠线程优先级。由于Java的线程是被映射到系统的原生线程上,因此线程调度最终仍是由操做系统说了算。如Windows中只有7种优先级,因此Java不得不出现几个优先级相同的状况。同时优先级可能会被系统自行改变。Windows系统中存在一个“优先级推动器”,当系统发现一个线程执行特别勤奋,可能会越过线程优先级为它分配执行时间。

线程安全的定义?

当多个线程访问一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替执行,也不须要进行额外的同步,或者在调用方法进行任何其余的协调操做,调用这个对象的行为均可以得到正确的结果,那这个对象就是线程安全的。

Java语言操做的共享数据,包括哪些?

  • 不可变
  • 绝对线程安全
  • 相对线程安全
  • 线程兼容
  • 线程对立

不可变

在Java语言里,不可变的对象必定是线程安全的,只要一个不可变的对象被正确构建出来,那其外部的可见状态永远也不会改变,永远也不会在多个线程中处于不一致的状态。

如何实现线程安全?

虚拟机提供了同步和锁机制。

  • 阻塞同步(互斥同步)
  • 非阻塞同步

阻塞同步(互斥同步)

互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。Java中最基本的同步手段就是synchronized关键字,其编译后会在同步块的先后分别造成monitorenter和monitorexit两个字节码指令。这两个字节码都须要一个Reference类型的参数指明要锁定和解锁的对象。若是Java程序中的synchronized明确指定了对象参数,那么这个对象就是Reference;若是没有明确指定,那就根据synchronized修饰的是实例方法仍是类方法,去获取对应的对象实例或Class对象做为锁对象。
在执行monitorenter指令时,首先要尝试获取对象的锁。

  • 若是这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexit指令时将锁计数器-1。当计数器为0时,锁就被释放了。
  • 若是获取对象失败了,那当前线程就要阻塞等待,知道对象锁被另一个线程释放为止。

除了synchronized以外,还可使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。ReentrantLock比synchronized增长了高级功能:等待可中断、可实现公平锁、锁能够绑定多个条件。

等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程能够选择放弃等待,对处理执行时间很是长的同步块颇有用。

公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次得到锁。synchronized中的锁是非公平的。

非阻塞同步

互斥同步最大的问题,就是进行线程阻塞和唤醒所带来的性能问题,是一种悲观的并发策略。老是认为只要不去作正确的同步措施(加锁),那就确定会出问题,不管共享数据是否真的会出现竞争,它都要进行加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程须要被唤醒等操做。

随着硬件指令集的发展,咱们可使用基于冲突检测的乐观并发策略。先进行操做,若是没有其余线程征用数据,那操做就成功了;若是共享数据有征用,产生了冲突,那就再进行其余的补偿措施。这种乐观的并发策略的许多实现不须要线程挂起,因此被称为非阻塞同步。

锁优化是在JDK的那个版本?

JDK1.6的一个重要主题,就是高效并发。HotSpot虚拟机开发团队在这个版本上,实现了各类锁优化:

  • 适应性自旋
  • 锁消除
  • 锁粗化
  • 轻量级锁
  • 偏向锁

为何要提出自旋锁?

互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操做都须要转入内核态中完成,这些操做给系统的并发性带来很大压力。同时不少应用共享数据的锁定状态,只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。先不挂起线程,等一下子。

自旋锁的原理?

若是物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,咱们只需让线程执行一个忙循环(自旋)。

自旋的缺点?

自旋等待自己虽然避免了线程切换的开销,但它要占用处理器时间。因此若是锁被占用的时间很短,自旋等待的效果就很是好;若是时间很长,那么自旋的线程只会白白消耗处理器的资源。因此自旋等待的时间要有必定的限度,若是自旋超过了限定的次数仍然没有成功得到锁,那就应该使用传统的方式挂起线程了。

什么是自适应自旋?

自旋的时间不固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  • 若是一个锁对象,自旋等待刚刚成功得到锁,而且持有锁的线程正在运行,那么虚拟机认为此次自旋仍然可能成功,进而运行自旋等待更长的时间。
  • 若是对于某个锁,自旋不多成功,那在之后要获取这个锁,可能省略掉自旋过程,以避免浪费处理器资源。

有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的情况预测就会愈来愈准确,虚拟机也会愈来愈聪明。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。

程序员怎么会在明知道不存在数据竞争的状况下使用同步呢?不少不是程序员本身加入的。

锁粗化

原则上,同步块的做用范围要尽可能小。可是若是一系列的连续操做都对同一个对象反复加锁和解锁,甚至加锁操做在循环体内,频繁地进行互斥同步操做也会致使没必要要的性能损耗。

锁粗化就是增大锁的做用域。

轻量级锁

在没有多线程竞争的前提下,减小传统的重量级锁使用操做系统互斥量产生的性能消耗。

偏向锁

消除数据在无竞争状况下的同步原语,进一步提升程序的运行性能。即在无竞争的状况下,把整个同步都消除掉。这个锁会偏向于第一个得到它的线程,若是在接下来的执行过程当中,该锁没有被其余的线程获取,则持有偏向锁的线程将永远不须要同步。
参考:《深刻理解Java虚拟机:JVM高级特性与最佳实践(第2版)》

写在最后

若是以为文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发编程技术。

最后,附上并发编程须要掌握的核心技能知识图,祝你们在学习并发编程时,少走弯路。

在这里插入图片描述

相关文章
相关标签/搜索