JVM结构的简单梳理

JVM是什么

JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是经过软件模拟物理机器执行程序的执行器.
JVM屏蔽了与具体操做系统平台相关的信息,使Java程序只需生成在JVM上运行的字节码,就能够在多种平台上不加修改地运行.
JVM在执行字节码时,实际上最终仍是把字节码解释成具体平台上的机器指令执行.java

JVMProcess.png


JVM的基本特性

  • 基于栈(Stack-based)的虚拟机 : 不一样于Intel x86和ARM等比较流行的计算机处理器都是基于寄存器(register)架构,JVM是基于栈执行的.
  • 符号引用(Symbolic reference) : 除基本类型外的全部Java类型(类和接口)都是经过符号引用取得关联的,而非显式的基于内存地址的引用.
  • 垃圾回收机制 : 类的实例经过用户代码进行显式建立,但却经过垃圾回收机制自动销毁.
  • 经过明确清晰基本类型确保平台无关性 : 像C/C++等传统编程语言对于int类型数据在同平台上会有不一样的字节长度.JVM却经过明确的定义基本类型的字节长度来维持代码的平台兼容性,从而作到平台无关.
  • 网络字节序(Network byte order) : Java class文件的二进制表示使用的是基于网络的字节序(network byte order).为了在使用小端(little endian)的Intel x86平台和在使用了大端(big endian)的RISC系列平台之间保持平台无关,必需要定义一个固定的字节序.JVM选择了网络传输协议中使用的网络字节序,即基于大端(big endian)的字节序.


JVM的流程结构

JVMStructure.png

1. Java编译(Java Compiler)

Java字节码是一种运行于Java和机器语言的中间语言,Java字节码也是部署Java程序的最小单元.
JVM自己就是用于执行Java字节码的执行器,因此'.java'源码文件要先编译为'.class'二进制字节码.编程

JavaCompiler.png

ps. javap -c/-verbose 能够将'.class'已可阅读方式输出数组

生成的'.class'文件由如下几部分组成 :
缓存

  • 结构信息 : 包括class文件格式版本号及各部分的数量与大小的信息.
  • 元数据 : 对应于Java源码中声明与常量的信息.包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池.
  • 方法信息 : 对应Java源码中语句和表达式对应的信息.包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息.

Java字节码中有4中表示调用方法的操做码 :
安全

  • invokeinterface: 调用接口方法
  • invokespecial: 调用初始化方法、私有方法、或父类中定义的方法
  • invokestatic: 调用静态方法
  • invokevirtual: 调用实例方法

2. 类加载子系统(Class Loader Subsystem)

注意! 类加载的几个阶段是按顺序开始,而不是按顺序进行或完成.
由于这些阶段一般都是互相交叉地混合进行的,一般在一个阶段执行的过程当中调用或激活另外一个阶段.bash

2.1 Loading

ClassLoading.png

中文名称 实现语言 做用
根加载器 C++ 在运行JVM时建立,用于加载JavaAPIs,包括Object类,不是ClassLoader子类
扩展加载器 Java 用于加载除基本JavaAPIs之外扩展类,也用于加载各类安全扩展功能
系统加载器 Java 加载应用程序相关的类与用户指定的ClassPath里的类
用户自定义加载器 Java 应用程序根据自身须要自定义的ClassLoader,如Tomcat、JBoss会根据J2EE规范自行实现ClassLoader
ps.
加载过程当中会先检查类是否被已加载,检查顺序是自底向上(见上图),
只要某个Classloader已加载就视为已加载此类,保证此类只全部 ClassLoader加载一次.
而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类(见上图).

2.2 Linking

ClassLinking.png

2.2.1. 验证(Verifying) :网络

验证类是否符合Java规范和JVM规范(编译阶段的语法语义分析不一样).
大部分TCK的测试用例都用于检测对于给定的错误的类文件是否能获得相应的验证错误信息.数据结构

TCK(Technology Compatibility Kit),由Oracle提供的测试工具.多线程

TCK经过执行大量的测试用例(包括大量经过不一样方式生成的错误类文件)来验证JVM规范.
只有经过TCK测试的JVM才能被称做是JVM.架构

相似TCK,还有一个JCP(http://jcp.org),用于验证新的Java技术规范.

对于一个JCP,必须具备详细的文档,相关的实现以及提交给JSR的TCK测试.
若是用户想像JSR同样使用新的Java技术,那他必须先从RI提供者那里获得许可,或者本身直接实现它并对之进行TCK测试.

ps. 
通常状况由javac编译的class文件是不会有问题的,
但可能有人的class文件是经过其余方式编译出来的,
这就有可能不符合JVM的编译规则,就须要过滤掉这部分不合法文件

2.2.2. 准备(Preparing) :

根据内存需求准备相应的数据结构,并分别描述出类中定义的字段方法以及实现的接口信息.
被final修饰的静态变量,会直接赋值为用户的定义值.
为类的静态变量分配内存,并设置类变量的初始值为默认值(不初始化静态代码块).

ps. '内存分配'仅包括类的静态变量,不包括实例变量,实例变量会在对象实例化时随对象一块分配在Java堆中

基本数据类型与referece的默认值,以下 :

数据类型 默认零值
int 0
long 0L
short (short)0
char '\u0000'
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

注意!

  1. 就基本数据类型来讲,对于类变量(static)和全局变量,若是不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来讲,在使用前必须显式地为其赋值,不然编译时不经过.
  2. 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,不然编译时不经过,而被final修饰的常量则既能够在声明时显式地为其赋值,也能够在类初始化时显式地为其赋值.总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值.
  3. 对于引用数据类型reference来讲,如数组引用、对象引用等,若是没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null.
  4. 若是在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值.

2.2.3. 解析(Resolving) :

将常量池中的全部符号引用(字面量描述)转为直接引用(对象和实例的地址指针、实例变量和方法的偏移量).
能够认为一些静态绑定的会被解析,动态绑定则只会在运行时进行解析.静态绑定包括一些final方法(不能够重写)、static方法(只会属于当前类)、构造器(不会被重写).

2.3 Initializing

这是类加载的最后阶段.为类的变量初始化合适的值.
若是执行的是静态变量,那么就会使用用户指定的值覆盖以前在准备阶段设置的初始值.
若是执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的全部操做.

注意!

  1. JVM必须确保一个类在初始化的过程当中,若是是多线程须要同时初始化它,仅仅只能容许其中一个线程对其执行初始化操做,其他线程必须等待,只有在活动线程执行完对类的初始化操做以后,才会通知正在等待的其余线程.
  2. 非静态类在实例化类,在Java堆中建立对象的时候,才会进行初始化,
    即在类被Java程序"第一次主动使用"的时候,才会触发初始化操做(若是尚未加载,则会顺势触发类的加载过程).

3. 运行时数据区(Runtime Data Areas)

RuntimeDataAreas.png

3.1 堆(Heap)

  1. JVM所管理的内存中最大的一块,是全部线程共享的一块内存区域,在JVM启动时建立.
  2. Heap是JVM用来存储对象实例以及数组值的区域,几乎全部的对象实例以及数组都在这里分配内存,Heap中的对象的内存须要等待GC进行回收.
  3. 因为Heap共享多个线程的内存,所存储的数据不是线程安全的.
  4. 如果在Heap中没有内存完成实例分配,而且Heap也没法再扩展时,将会抛出OutOfMemoryError异常.
  5. 注意.Heap空间的大小JVM、是否不执行垃圾回收,包括晋升老年代的年龄阀值等等配置都是能够修改设置的.
  6. 已知Heap是全部线程共享的,所以在其上进行对象内存的分配是须要进行加锁,这也致使了new对象的开销是比较大的.
  7. Oracle Hotspot JVM为了提高对象内存分配的效率,对于所建立的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),

    其大小由JVM根据运行的状况计算而得,在TLAB上分配对象时不须要加锁,所以JVM在给线程的对象分配内存时会尽可能的在TLAB上分配,

    以上状况下JVM中分配对象内存的性能和C基本是同样高效的,但若是对象过大的话则仍然是直接使用堆空间分配.
  8. TLAB仅做用于新生代的Eden空间,所以在编写Java程序时,一般多个小的对象比大的对象分配起来更加高效.
3.1.1 GC堆(Garbage Collected Heap)

GCHeap.png

Heap划分为两大块 :

  1. 新生代(Young Generation)
# 其分为如下几块区域
# Eden Space : 任何新进入运行时数据区域的实例都会存放在此
# S0 Survivor Space : 存在时间较长,通过垃圾回收没有被清除的实例,就从Eden搬到了S0
# S1 Survivor Space : 存在时间更长的实例,就从S0搬到了S1

全部新建立的Object都将会存储在新生代中.晋升到老年代有如下几种状况 :

  1. 每经历一次垃圾回收,对象的年龄加1(首次进Survivor区后初始年龄为1),当增长至必定程度(默认为15)时,晋升为老年代.
  2. 当一次Minor GC后,对象不够Survivor区彻底容纳,会直接晋升为老年代.
  3. 当新生代的相同年龄的对象超过Survivor区的50%时,年龄大于或等于其相同年龄的对象,会直接晋升为老年代.
  1. 老年代(Old/Tenured Generation)
# Tenured : 主要存放应用程序中生命周期长的内存对象

老年代的对象比较稳定,因此Major GC不会频繁执行.触发Major GC(Full GC)有如下状况 :

  1. 当有新生代的对象晋升入老年代,致使空间不够用时才触发Major GC.
  2. 当没法找到足够大的连续空间分配给新建立的较大对象时也会提早触发一次Major GC进行垃圾回收腾出空间.
  3. 发生Minor GC时,JVM会检测以前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,
    若是大于,则进行一次Major GC,
    若是小于,则查看HandlePromotionFailure设置是否容许担保失败,
    若是容许,那只会进行一次Minor GC,
    若是不容许,则改成进行一次Major GC.

Major GC的耗时比较长,须要先扫描再回收,且为了减小内存碎片致使的内存损耗,通常都须要进行合并整理方便下次直接分配.
老年代也会存在内存容量不过的状况,也会抛出OutOfMemoryError异常.

ps. JDK1.8,方法区(HotSpot的永久代(Permanent Generation)),已替换为元空间(Metaspace),使用的是直接内存,受本机可用内存的限制,而且永远不会获得java.lang.OutOfMemoryError(可限制大小).

3.1.2 运行时常量池(Runtime Constant Pool)

ConstantPoolInfo.png

ps. JDK1.7及以后版本的JVM已经将运行时常量池从方法区中移了出来,在Java Heap中开辟了一块区域存放运行时常量池

Class文件中有类的版本、字段、方法、接口等描述信息外,还有常量池信息(见上图,用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后存放到常量池中).
当常量池没法再申请到内存时是会抛出OutOfMemoryError异常.

ps. 运行时常量池中的内容主要是从各个类型的class文件的常量池中获取

!注意!
运行时常量是相对于常量来讲的,它具有一个重要特征是 : 动态性
值相同的动态常量与咱们一般说的常量只是来源不一样,可是都是储存在池内同一块内存区域.
Java并不要求常量必定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中.
这里所说的常量包括:基本类型包装类(包装类无论理浮点型,整形只会管理-128到127)和String类(也能够经过String.intern()方法能够强制将String放入常量池).

常量池是为了不频繁的建立和销毁对象而影响系统性能,其实现了对象的共享.
例如字符串常量池,在编译阶段就把全部的字符串文字放到一个常量池中.

节省内存空间:常量池中全部相同的字符串常量被合并,只占用一个空间.
节省运行时间:比较字符串时,==比equals()快.对于两个引用变量,只用==判断引用是否相等,也就能够判断实际值是否相等.

3.2 程序计数器(Program Counter Register)

每一个线程都会有一个程序计数器,各线程之间计数器互不影响,独立储存,咱们称这类内存区域为"线程私有"的内存.
程序计数器是一块较小的内存空间.当字节码解释器工做时,经过改变这个计数器的值来选取下一条须要执行的字节码指令.

程序计数器主要有两个做用 :

  1. 字节码解释器经过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、分支、选择、循环、跳转、异常处理、线程恢复等基础功能.
  2. 在多线程的状况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候可以知道该线程上次运行到哪儿了.

ps. 程序计数器是惟一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的启动而建立,随着线程的结束而死亡.
ps. 若下一步执行的指令为Native的话,则PC寄存器中不存储任何信息.

3.3 本地方法栈(Native Method Stack)

为非Java编写的本地代程定义的栈空间.也就是说它基本上是用于经过JNI(Java Native Interface)方式调用和执行的C/C++代码,根据具体状况,C栈或C++栈将会被建立.
此区域还用于存储每一个native方法调用的状态.

本地方法栈虚拟机栈所发挥的做用很是类似,区别是:

  • 虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务.
  • 本地方法被执行的时候,在本地方法栈也会建立一个栈帧,用于存放该本地方法的局部变量表、操做数栈、动态连接、出口信息.
  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现StackOverFlowError和OutOfMemoryError两种异常.

ps. 本地方法栈在HotSpot JVM中与虚拟机栈合二为一

3.4 虚拟机栈(VM Stack)

VMStack.png

虚拟机栈是线程私有的,不能被任何其余线程引用,并跟随线程的启动而建立.其中存储的数据无素称为栈帧(Stack Frame).
虚拟机栈会拥有多个栈帧(Stack Frame).JVM会把栈桢压入虚拟机栈或从中弹出一个栈帧.
若是有任何异常抛出,如printStackTrace()方法输出的栈跟踪信息的每一行表示一个栈帧.

注意.
1. 若是线程中的计算须要比容许的更大的虚拟机栈,则JVM会抛出一个StackOverflowError.
2. 若是能够动态扩展虚拟机栈,而且尝试进行扩展但内存不足以实现扩展,或者能够内存不足觉得新线程建立初始虚拟机栈,则JVM抛出一个OutOfMemoryError.
3.4.1 栈帧(Stack Frame)

栈帧用于存储数据部分结果动态连接返回地址调度异常以及属于当前运行方法的运行时常量池的引用等信息.
本地变量数组和操做数栈的大小在编译时就已肯定,因此属在运行时属于方法的栈帧大小是固定的.
因为除了推送和弹出帧以外,永远不会直接操做虚拟机栈.虚拟机栈的内存不须要是连续的.

ps.不管是'return语句'仍是'抛出异常',无论哪一种返回方式都会致使栈帧被弹出.

3.4.1.1 局部变量(Local Variables)

每一个栈帧包含一个称为局部变量的变量数组,用于存放方法参数和方法内定义的局部变量.

帧的局部变量数组的长度在编译时肯定,并以类或接口的二进制表示形式提供,同时提供与帧相关的方法的代码.
JVM使用局部变量在方法调用上传递参数.在类方法调用中,任何参数都从局部变量0开始的连续局部变量中传递.
在实例方法调用中,局部变量0老是用于传递对调用实例方法的对象的引用.
随后,任何参数都在从局部变量1开始的连续局部变量中传递,以及其后的就是真正的方法的本地变量.

局部变量表的容量以变量槽(Variable Slot)为最小单位 :
一个Slot能够存放一个32位之内(boolean、byte、char、short、int、float、reference和returnAddress)的数据类型,reference类型表示一个对象实例的引用,returnAddress已经不多见了,能够忽略.
对于64位的数据类型(Java语言中明确的64位数据类型只有long和double),JVM会以高位对齐的方式为其分配两个连续的Slot空间.

3.4.1.2 操做数栈(Operand Stacks)

OperandStacks.png

ps.
在概念模型中,一个活动线程中两个栈帧是相互独立的.但大多数虚拟机实现都会作一些优化处理,
即上图,让下一个栈帧的部分操做数栈与上一个栈帧的部分局部变量表重叠在一块儿,这样的好处是方法调用时能够共享一部分数据,而无须进行额外的参数复制传递.

每一个栈帧包含一个后进先出(LIFO)堆栈,称为其操做数栈,帧的操做数栈的最大深度在编译时已肯定,并与用于与帧相关的方法的代码一块儿提供.
当一个方法执行开始时,这个方法的操做数栈是空的,在方法执行过程当中,会有各类字节码指令往操做数栈中写入和提取内容,也就是出栈/入栈操做.
JVM提供指令以将局部变量或字段中的常量或值加载到操做数栈上.
其余JVM指令从操做数栈中获取操做数,对它们进行操做,并将结果推回操做数栈.
操做数栈还用于准备要传递给方法和接收方法结果的参数.

3.4.1.3 动态链接(Dynamic Linking)

每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接.
字节码中方法调用指令是以常量池中的指向方法的符号引用为参数的,动态连接将这些符号方法引用转换为具体的方法引用.

ps. 注意!有一部分符号引用会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析,另一部分在每次的运行期间转化为直接引用,这部分称为动态链接.

3.4.1.4 返回地址(Return Address)

当一个方法被执行后,有两种方式退出这个方法:

  1. 正常方法调用完成(Normal Method Invocation Completion)

    若是调用不会直接从Java虚拟机或执行显式throw语句引起异常,则方法调用会正常完成.
    若是当前方法的调用正常完成,则能够将值返回给调用方法.
    当被调用的方法执行其中一个返回指令时,就会发生这种状况,返回指令的选择必须适合于返回值的类型(若是有的话).
    这种退出方法的方式是,执行引擎遇到任意一个方法返回的字节码指令.

  1. 忽然方法调用完成(Abrupt Method Invocation Completion)

    在方法执行过程当中遇到了异常,且这个异常没有在方法体内获得处理(即本方法异常处理表中没有匹配的异常处理器),则方法调用会忽然完成,就致使方法退出.
    这种退出方式不会给上层调用者产生任何返回值.

不管采用何种退出方式,在方法退出后,都须要返回到方法被调用的位置,程序才能继续执行,
方法返回时可能须要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态.
通常来讲,
方法正常退出时,调用者的程序计数器的值能够做为返回地址,栈帧中极可能会保存这个计数器值.
而方法异常退出 时,返回地址是经过异常处理器表来肯定的,栈帧中通常不会保存这部分信息.

ps.
方法退出的过程实际上等同于把当前栈帧出栈,所以退出时可能执行的操做有 : 
恢复上层方法的局部变量表和操做数栈,把返回值(若是有的话)压入调用者栈帧的操做数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等.

4. 元空间(Metaspace)

Metaspace.png

ps. JDK 1.8以后,方法区(Oracle Hotspot JVM)的永久代被完全移除了,取而代之是元空间,元空间使用的是直接内存

Metaspace的组成 :

  1. Klass Metaspace:
    Klass Metaspace就是用来存klass的,klass是咱们熟知的class文件在JVM里的运行时数据结构.
    咱们看到的相似A.class实际上是存在Heap里的,是java.lang.Class的一个对象实例.
    这块内存是紧接着Heap的,和以前的永久代同样,这块内存大小可经过-XX:CompressedClassSpaceSize参数来控制,
    这个参数默认是1G,可是这块内存也能够没有,假如没有开启压缩指针就不会有这块内存,这种状况下klass都会存在NoKlass Metaspace里,
    另外若是咱们把-Xmx设置大于32G的话,其实也是没有这块内存的,由于会这么大内存会关闭压缩指针开关.
    还有就是这块内存最多只会存在一块.
  2. NoKlass Metaspace:
    NoKlass Metaspace专门来存klass相关的其余的内容,好比Method,ConstantPool等.
    这块内存是由多块内存组合起来的,因此能够认为是不连续的内存块组成的.
    这块内存是必须的,虽然叫作NoKlass Metaspace,可是也其实能够存klass的内容,在第一点已经提到了.
ps. 
Klass Metaspace和NoKlass Mestaspace都是全部classloader共享的,因此类加载器们要分配内存,
可是每一个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块.
若是Klass Metaspace用完了,那就会OutOfMemoryError,不过通常状况下不会,
NoKlass Mestaspace是由一块块内存慢慢组合起来的,在没有达到限制条件的状况下,会不断加长这条链,让它能够持续工做.

Metaspace的特色 :

  1. 类及相关的元数据的生命周期与类加载器的一致.
  2. 类的元数据放入Native Memory,字符串池和类的静态变量放入Java Heap中.
  3. 每一个加载器有专门的存储空间.
  4. 只进行线性分配.
  5. 不会单独回收某个类.
  6. 省掉了GC扫描及压缩的时间(永久代会为GC带来没必要要的复杂度,而且回收效率偏低).
  7. 元空间里的对象的位置是固定的.
  8. 若是GC发现某个类加载器再也不存活了,会把相关的空间整个回收掉.

5. 执行引擎(Execution Engine)

JVM经过类加载器把字节码载入运行时数据区,由执行引擎执行.
执行引擎以指令为单位读入Java字节码,就像CPU一个接一个的执行机器命令同样.
每一个字节码命令包含一字节的操做码和可选的操做数.执行引擎读取一个指令并执行相应的操做数,而后去读取并执行下一条指令.

5.1 解释器(Interpreter)

读取解释逐一执行每一条字节码指令.
由于解释器逐一解释和执行指令,解释器解释字节码的速度更快,但对解释结果的执行速度较慢.全部的解释性语言都有相似的缺点.
解释器的缺点是,当屡次调用一种方法时,每次都须要新的解释.

5.2 即时编译器(JIT(Just-In-Time) Compiler)

JITCompiler.png

即时编译器的引入用来弥补解释器的不足.执行引擎先以解释器的方式运行,而后在合适的时机,即时编译器把整修字节码编译成本地代码.

首先,即时编译器先把字节码经过中间代码生成器(Itermediate Representation Generator)转为一种中间形式的表达式.
而后,代码优化器(Code Optimizer)负责优化上面生成的代码.
最后,目标代码生成器(Target Code Generator)负责生成机器代码或本地代码.
期间,Profiler会负责查找热点代码,即该方法是否被屡次调用.

ps.
Oracle Hotspot VM使用的即时编译器称为Hotspot编译器.
之因此称为Hotspot是由于Hotspot Compiler会根据分析找到具备更高编译优先级的热点代码,而后所这些热点代码转为本地代码.而且经过对本地代码的缓存,编译后的代码能具备更快的执行速度.
若是一个被编译过的方法再也不被频繁调用,也即再也不是热点代码,Hotspot VM会把这些本地代码从缓存中删除并对其再次使用解释器模式执行.
Hotspot VM有Server VM和Client VM以后,它们所使用的即时编译器也有所不一样.

5.3 GC收集器(Garbage Collector)

GC是后台的守护进程. 有多种且针对不一样区域的GC收集器(如.Serial收集器、CMS(Concurrent Mark Sweep)收集器、G1(GarbageFirst)收集器等等).

相关文章
相关标签/搜索