谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

1:什么是JVM

你们能够想一想,JVM 是什么?JVM是用来干什么的?在这里我列出了三个概念,第一个是JVM,第二个是JDK,第三个是JRE。相信你们对这三个不会很陌生,相信大家都用过,可是,大家对这三个概念有清晰的知道么?我不知道大家会不会,知不知道。接下来大家看看我对JVM的理解。
java

(1):JVM
程序员

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是经过在实际的计算机上仿真模拟各类计算机功能来实现的。算法

引入Java语言虚拟机后,Java语言在不一样平台上运行时不须要从新编译。编程

Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,小程序

使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),数组

就能够在多种平台上不加修改地运行。浏览器

Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。缓存

这就是Java的可以“一次编译,处处运行”的缘由。性能优化

(2):JDK
bash

JDK(Java Development Kit) 是 Java 语言的软件开发工具包(SDK)。

JDK包含的基本组件包括:

  1. javac – 编译器,将源程序转成字节码

  2. jar – 打包工具,将相关的类文件打包成一个文件

  3. javadoc – 文档生成器,从源码注释中提取文档

  4. jdb – debugger,查错工具

  5. java – 运行编译后的java程序(.class后缀的)

  6. appletviewer:小程序浏览器,一种执行HTML文件上的Java小程序的Java浏览器。

  7. Javah:产生能够调用Java过程的C过程,或创建能被Java程序调用的C过程的头文件。

  8. Javap:Java反汇编器,显示编译类文件中的可访问功能和数据,同时显示字节代码含义。

  9. Jconsole: Java进行系统调试和监控的工具

(3):JRE

JRE(Java Runtime Environment,Java运行环境),运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。

包括两部分:

Java Runtime Environment:

  • 是能够在其上运行、测试和传输应用程序的Java平台。

  • 它包括Java虚拟机(jvm)、Java核心类库和支持文件。

  • 它不包含开发工具(JDK)--编译器、调试器和其它工具。

  • JRE须要辅助软件--Java Plug-in--以便在浏览器中运行applet。

Java Plug-in。

容许Java Applet和JavaBean组件在使用Sun的Java Runtime Environment(JRE)的浏览器中运行,

而不是在使用缺省的Java运行环境的浏览器中运行。

Java Plug-in可用于Netscape Navigator和Microsoft Internet Explorer。

2:JVM运行时数据区

Java虚拟机在执行Java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。这些区域都有各自的用途,已经建立和销毁时间,有的区域随着虚拟机进程的启动而建立,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(Java SE 7)》的规定,Java虚拟机所管理的内存将会包括如下几个运行时数据区域,以下图所示:

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

2.一、程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它能够看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各类虚拟机可能会经过一些更高效的方式去实现),字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。

因为Java虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的。在任何一个肯定的时刻,一个处理器都只会执行一条线程中的指令。所以,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。

若是线程正在执行的是一个Java方法,那这个计数器记录的是正在执行的字节码指令的地址;若是正在执行的是Native方法,这个计数器值则为空(undefined)。

此内存区域是惟一一个在Java虚拟机规范中没有规定任何OutOfMemoryError状况的区域。

程序计数器是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)。

2.二、Java虚拟机栈

虚拟机栈(Java Virtual Machine Stack)描述的是Java方法执行的内存模型:每一个方法被执行的同时都会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

在Java虚拟机规范中,对这个区域规定了两种异常状况:

若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常;

若是虚拟机栈能够动态扩展(当前大部分的Java虚拟机均可以扩展),若是扩展时没法申请到足够的内存,就会抛出OutOfMemoryError异常。

与程序寄存器同样,java虚拟机栈也是线程私有的,它的生命周期与线程相同。

2.三、本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的做用是很是相似,它们之间的区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并无强制规定,所以具体的虚拟机能够自由的实现它。

与虚拟机栈同样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

与虚拟机栈同样,本地方法栈也是线程私有的。

2.四、Java 堆(Java Heap)

对于大多数应用来讲,Java 堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java 堆是被全部线程共享的一块内存区域,在虚拟机启动的是建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例以及数组都要在这里分配内存。

Java堆是垃圾收集器管理的主要区域,所以不少时候也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,因为如今收集器基本都采用分代收集算法,因此Java堆还能够细分为:新生代和老年代;新生代又能够分为:Eden 空间、From Survivor空间、To Survivor空间。

根据Java虚拟机规范的规定,Java堆能够处于物理上不连续的内存空间中,只要逻辑上是连续的便可,就像咱们的磁盘空间同样。在实现时,既能够实现成固定大小的,也能够是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(经过-Xms和-Xmx控制)。若是在堆中没有内存完成实例的分配,而且堆也没法再扩展时,将会抛出OutOfMemoryError异常。

2.五、方法区(Method Area)

方法区(Method Area)和Java堆同样,是各个线程共享的内存区域,它用于存放已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据。方法区在虚拟机启动的时候建立。

Java虚拟机规范对方法区的限制很是宽松,除了和堆同样不须要不连续的内存空间和能够固定大小或者可扩展外,还能够选择不实现垃圾收集。

根据Java虚拟机规范的规定,若是方法区的内存空间不能知足内存分配须要时,将抛出OutOfMemoryError异常。

2.六、运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。

2.七、直接内存

直接内存(Direct Memory)并非虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。可是这部份内存也被频繁使用,并且也可能致使OutOfMemoryError异常出现。

在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方法,它可使用Native函数库直接分配堆外内存,而后经过一个存储在Java堆中DirectByteBuffer对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在Java堆和Native堆中来回复制数据。

到这里咱们大体知道了Java虚拟机的运行时区的概况,接下来会继续介绍更多JVM相关信息。

在此我向你们推荐一个交流学习群:575745314 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

3:JVM内存模型

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工做方式。JVM是整个计算机虚拟模型,因此JMM是隶属于JVM的。

若是咱们要想深刻了解Java并发编程,就要先理解好Java内存模型。Java内存模型定义了多线程之间共享变量的可见性以及如何在须要的时候对共享变量进行同步。原始的Java内存模型效率并非很理想,所以Java1.5版本对其进行了重构,如今的Java8仍沿用了Java1.5的版本。

关于并发编程

在并发编程领域,有两个关键问题:线程之间的通讯和同步。

线程之间的通讯

线程的通讯是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通讯机制有两种共享内存和消息传递。

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间经过写-读内存中的公共状态来隐式进行通讯,典型的共享内存通讯方式就是经过共享对象进行通讯。

在消息传递的并发模型里,线程之间没有公共状态,线程之间必须经过明确的发送消息来显式进行通讯,在java中典型的消息传递方式就是wait()和notify()。

关于Java线程之间的通讯,能够参考线程之间的通讯(thread signal)。

线程之间的同步

同步是指程序用于控制不一样线程之间操做发生相对顺序的机制。

在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码须要在线程之间互斥执行。

在消息传递的并发模型里,因为消息的发送必须在消息的接收以前,所以同步是隐式进行的。

Java的并发采用的是共享内存模型

Java线程之间的通讯老是隐式进行,整个通讯过程对程序员彻底透明。若是编写多线程程序的Java程序员不理解隐式进行的线程之间通讯的工做机制,极可能会遇到各类奇怪的内存可见性问题。

Java内存模型

上面讲到了Java线程之间的通讯采用的是过共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

从上图来看,线程A与线程B之间如要通讯的话,必需要经历下面2个步骤:

1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。2. 而后,线程B到主内存中去读取线程A以前已更新过的共享变量。复制代码

下面经过示意图来讲明这两个步骤:

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在本身的本地内存A中。当线程A和线程B须要通讯时,线程A首先会把本身本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从总体来看,这两个步骤实质上是线程A在向线程B发送消息,并且这个通讯过程必需要通过主内存。JMM经过控制主内存与每一个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

上面也说到了,Java内存模型只是一个抽象概念,那么它在Java中具体是怎么工做的呢?为了更好的理解上Java内存模型工做方式,下面就JVM对Java内存模型的实现、硬件内存模型及它们之间的桥接作详细介绍。

JVM对Java内存模型的实现

在JVM内部,Java内存模型把内存分红了两部分:线程栈区和堆区,下图展现了Java内存模型在JVM中的逻辑视图:

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

JVM中运行的每一个线程都拥有本身的线程栈,线程栈包含了当前线程执行的方法调用相关信息,咱们也把它称做调用栈。随着代码的不断执行,调用栈会不断变化。

线程栈还包含了当前方法的全部本地变量信息。一个线程只能读取本身的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即便两个线程执行的是同一段代码,它们也会各自在本身的线程栈中建立本地变量,所以,每一个线程中的本地变量都会有本身的版本。

全部原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的本地变量,一个线程能够传递一个副本给另外一个线程,当它们之间是没法共享的。

堆区包含了Java应用建立的全部对象信息,无论对象是哪一个线程建立的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。无论对象是属于一个成员变量仍是方法中的本地变量,它都会被存储在堆区。

下图展现了调用栈和本地变量都存储在栈区,对象都存储在堆区:

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

一个本地变量若是是原始类型,那么它会被彻底存储到栈区。

一个本地变量也有多是一个对象的引用,这种状况下,这个本地引用会被存储到栈中,可是对象自己仍然存储在堆区。

对于一个对象的成员方法,这些方法中包含本地变量,仍须要存储在栈区,即便它们所属的对象在堆区。

对于一个对象的成员变量,无论它是原始类型仍是包装类型,都会被存储到堆区。

Static类型的变量以及类自己相关信息都会随着类自己存储在堆区。

堆中的对象能够被多线程共享。若是一个线程得到一个对象的应用,它即可访问这个对象的成员变量。若是两个线程同时调用了同一个对象的同一个方法,那么这两个线程即可同时访问这个对象的成员变量,可是对于本地变量,每一个线程都会拷贝一份到本身的线程栈中。

下图展现了上面描述的过程:

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

硬件内存架构

无论是什么内存模型,最终仍是运行在计算机硬件上的,因此咱们有必要了解计算机硬件内存架构,下图就简单描述了当代计算机硬件内存架构:

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

现代计算机通常都有2个以上CPU,并且每一个CPU还有可能包含多个核心。所以,若是咱们的应用是多线程的话,这些线程可能会在各个CPU核心中并行运行。

在CPU内部有一组CPU寄存器,也就是CPU的储存器。CPU操做寄存器的速度要比操做计算机主存快的多。在主存和CPU寄存器之间还存在一个CPU缓存,CPU操做CPU缓存的速度快于主存但慢于CPU寄存器。某些CPU可能有多个缓存层(一级缓存和二级缓存)。计算机的主存也称做RAM,全部的CPU都可以访问主存,并且主存比上面提到的缓存和寄存器大不少。

当一个CPU须要访问主存时,会先读取一部分主存数据到CPU缓存,进而在读取CPU缓存到寄存器。当CPU须要写数据到主存时,一样会先flush寄存器到CPU缓存,而后再在某些节点把缓存数据flush到主存。

Java内存模型和硬件架构之间的桥接

正如上面讲到的,Java内存模型和硬件内存架构并不一致。硬件内存架构中并无区分栈和堆,从硬件上看,无论是栈仍是堆,大部分数据都会存到主存中,固然一部分栈和堆的数据也有可能会存到CPU寄存器中,以下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系:

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

当对象和变量存储到计算机的各个内存区域时,必然会面临一些问题,其中最主要的两个问题是:

1. 共享对象对各个线程的可见性2. 共享对象的竞争现象123复制代码

共享对象的可见性

当多个线程同时操做同一个共享对象时,若是没有合理的使用volatile和synchronization关键字,一个线程对共享对象的更新有可能致使其它线程不可见。

想象一下咱们的共享对象存储在主存,一个CPU中的线程读取主存数据到CPU缓存,而后对共享对象作了更改,但CPU缓存中的更改后的对象尚未flush到主存,此时线程对共享对象的更改对其它CPU中的线程是不可见的。最终就是每一个线程最终都会拷贝共享对象,并且拷贝的对象位于不一样的CPU缓存中。

下图展现了上面描述的过程。左边CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改成2。但这个变动对运行在右边CPU中的线程不可见,由于这个更改尚未flush到主存中:

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

要解决共享对象可见性这个问题,咱们可使用java volatile关键字。 Java’s volatile keyword. volatile 关键字能够保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU内存屏障指令实现的,后面会讲到。

竞争现象

若是多个线程共享一个对象,若是它们同时修改这个共享对象,这就产生了竞争现象。

以下图所示,线程A和线程B共享一个对象obj。假设线程A从主存读取Obj.count变量到本身的CPU缓存,同时,线程B也读取了Obj.count变量到它的CPU缓存,而且这两个线程都对Obj.count作了加1操做。此时,Obj.count加1操做被执行了两次,不过都在不一样的CPU缓存中。

若是这两个加1操做是串行执行的,那么Obj.count变量便会在原始值上加2,最终主存中的Obj.count的值会是3。然而下图中两个加1操做是并行的,无论是线程A仍是线程B先flush计算结果到主存,最终主存中的Obj.count只会增长1次变成2,尽管一共有两次加1操做。

谁说深刻浅出虚拟机难?如今我让他通俗易懂(JVM)

要解决上面的问题咱们可使用java synchronized代码块。synchronized代码块能够保证同一个时刻只能有一个线程进入代码竞争区,synchronized代码块也能保证代码块中全部变量都将会从主存中读,当线程退出代码块时,对全部变量的更新将会flush到主存,无论这些变量是否是volatile类型的。

volatile和 synchronized区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工做内存)中的值是不肯定的,须要从主存中读取; synchronized则是锁定当前变量,只有当前线程能够访问该变量,其余线程被阻塞住。

  2. volatile仅能使用在变量级别;synchronized则可使用在变量、方法、和类级别的

  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则能够保证变量的修改可见性和原子性

  4. volatile不会形成线程的阻塞;synchronized可能会形成线程的阻塞。

  5. volatile标记的变量不会被编译器优化;synchronized标记的变量能够被编译器优化

支撑Java内存模型的基础原理

指令重排序

在执行程序时,为了提升性能,编译器和处理器会对指令作重排序。可是,JMM确保在不一样的编译器和不一样的处理器平台之上,经过插入特定类型的Memory Barrier来禁止特定类型的编译器重排序和处理器重排序,为上层提供一致的内存可见性保证。

  1. 编译器优化重排序:编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。

  2. 指令级并行的重排序:若是不存l在数据依赖性,处理器能够改变语句对应机器指令的执行顺序。

  3. 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操做看上去多是在乱序执行。

数据依赖性

若是两个操做访问同一个变量,其中一个为写操做,此时这两个操做之间存在数据依赖性。

编译器和处理器不会改变存在数据依赖性关系的两个操做的执行顺序,即不会重排序。

as-if-serial

无论怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵照as-if-serial语义。

内存屏障(Memory Barrier )

上面讲到了,经过内存屏障能够禁止特定类型处理器的重排序,从而让程序按咱们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:

  1. 保证特定操做的执行顺序。

  2. 影响某些数据(或则是某条指令的执行结果)的内存可见性。

编译器和CPU可以重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:无论什么指令都不能和这条Memory Barrier指令重排序。

Memory Barrier所作的另一件事是强制刷出各类CPU cache,如一个Write-Barrier(写入屏障)将刷出全部在Barrier以前写入 cache 的数据,所以,任何CPU上的线程都能读取到这些数据的最新版本。

这和java有什么关系?上面java内存模型中讲到的volatile是基于Memory Barrier实现的。

若是一个变量是volatile修饰的,JMM会在写入这个字段以后插进一个Write-Barrier指令,并在读这个字段以前插入一个Read-Barrier指令。这意味着,若是写入一个volatile变量,就能够保证:

  1. 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。

  2. 在写入变量a以前的写入操做,其更新的数据对于其余线程也是可见的。由于Memory Barrier会刷出cache中的全部先前的写入。

happens-before

从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操做之间的内存可见性。

在JMM中,若是一个操做的执行结果须要对另外一个操做可见,那么这两个操做之间必需要存在happens-before关系,这个的两个操做既能够在同一个线程,也能够在不一样的两个线程中。

与程序员密切相关的happens-before规则以下:

  1. 程序顺序规则:一个线程中的每一个操做,happens-before于该线程中任意的后续操做。

  2. 监视器锁规则:对一个锁的解锁操做,happens-before于随后对这个锁的加锁操做。

  3. volatile域规则:对一个volatile域的写操做,happens-before于任意线程后续对这个volatile域的读。

  4. 传递性规则:若是 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:两个操做之间具备happens-before关系,并不意味前一个操做必需要在后一个操做以前执行!仅仅要求前一个操做的执行结果,对于后一个操做是可见的,且前一个操做按顺序排在后一个操做以前。

在此我向你们推荐一个交流学习群:575745314 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多。

相关文章
相关标签/搜索