深刻理解java虚拟机-虚拟机执行子系统

1. class类文件结构

  1. class文件是以8字节为基础单位的二进制流,中间没有添加任何分隔符
  2. 根据java虚拟机规范的规定,class文件格式采用相似c语言结构体的伪结构来存储数据,这种结构只有两种类型:无符号数和表
  1. 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别表示1个字节,2个字节,4个字节和8个字节的无符号数,无符号数用来描述数字,索引引用,数量值或按照UTF8编码构成字符串数
  2. 表是由多个无符号数或其余表做为数据项构成的复合数据类型,全部表都习惯性的以"_info"结尾,表用于描述有层次关系的复合结构的数据。整个CLASS文件本质上也是一张表。
  1. 魔数(4个字节):每一个Class文件的头4个字节称为魔数(Magic Number),它的惟一做用是肯定这个文件是否为一个能被虚拟机接受的Class文件,第5个字节和第6个字节是次版本号,第7个字节和第8个字节是主版本号

不少文件存储标准都是用魔数来进行身份识别,好比jpg,jpeg等在文件头都存着魔数
查看方式,使用十六进制编辑器打开能够看到java

class文件格式
image程序员

  1. 常量池计数器(2个字节)
      主版本以后的即为常量池,一般存放的是class文件的资源仓库
      常量池容量计数值,计数器从1而不是0开始,0x0016,十进制为22,表明有21个常量。索引为1~21.没有使用0索引是由于在后面某些指向常量池的索引能够经过0索引表示不引用任何一个常量池项目的意思。

这样作的目的是为了区分其余数据类型不引用常量池,好比接口索引集合,字段表索引集合,方法表集合等容量计数都是从0开始的web

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)数据库

字面量:数组

  • 文本字符串
  • 声明未final的常量值

符号引用:安全

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

JAVA代码在进行JAVAC编译时,并不像C和C++那样有链接这一步骤,而是在虚拟机加载CLASS文件的时候进行动态链接。也就是说,在CLASS文件中不会保存各个方法和字段的最终内存布局信息,所以这些字段和方法的符号引用不通过转换的话是没法直接被虚拟机使用的。当虚拟机运行时,须要从常量池得到对应的符号引用,在类建立时或运行时解析、翻译到具体的内存地址中。服务器

在这里插入图片描述

常量池中每一项常量都是一个表,jdk1.7以前共有11种,表开始的第一位都是一个字节的标志位,代表这个常量属于哪一种类型。网络

JAVA程序中不能定义超过64KB英文字符的变量和方法名,不然没法编译数据结构

使用JAVAP工具能够分析class文件字节码架构

javap -verbose TestClass

  1. 访问标志

在常量池以后,紧接着2个字节为访问标志,用于识别一些类或接口的访问信息,包括这个class是类仍是接口,是否为public,是否为abstract等

在这里插入图片描述

  1. 类索引、父类索引与接口索引集合

类索引和父类索引都是u2类型的数据,而接口索引集合是一组u2类型数据的组合,class文件中由这三项用来肯定继承关系

类索引、父类索引和接口索引集合都是按照顺序排列在访问标志以后

  1. 字段表集合

用于描述接口或类中声明的变量。字段field包括了类级变量或实例级变量,但不包括在方法内部声明的变量。
字段修饰符(access_flags)结构:描述了该字段是不是Public,private, protected, static,final,volatile,transient等
在这里插入图片描述
在字段表结构以后有两项索引值:name_ index,descriptor_index,他们都是对常量池的引用,分别表明字段的简单名称以及字段和方法及描述符

  • 简单名称:没有类型和参数修饰的方法或者字段名称
  • 描述符:用来描述字段的数据类型,方法的参数列表(数量,类型,顺序),和返回值
    在这里插入图片描述

在java中字段是没法重载的,两个字段的数据类型,修饰符无论是否相同,都必须使用不同的名称,可是对于字节码文件来讲,若是两个字段的描述符若是不同的话,那么内容是能够重复的

  1. 方法表集合

描述方法

包括访问标志,名称索引,描述符索引,属性表索引。方法中的代码储存在方法属性表集合中一个名为Code的属性中。

在JAVA虚拟机规范中,要重载一个方法,除了要与原方法具备相同的简单名称外,还要求必须拥有一个与原方法不一样的特征签名,特征签名在JAVA代码层面和字节码层面有不一样的定义。代码层面的签名只包括方法名称,参数顺序及参数类型,字节码层面还包括方法返回值和异常表。所以在CLASS文件中,若是两个方法有相同的名称和特征签名,但返回值不一样,是合法的

  1. 属性表集合

属性表集合的限制较少,不要求各个属性表有严格的顺序,而且只要不与已有的属性名重复,任何人实现的编译器均可以向属性表中写入本身定义的属性信息。JAVA虚拟机在运行时会忽略掉它不认识的属性。JAVA虚拟机规范中预约义了9项虚拟机实现应当能识别的属性,在最新的java虚拟机规范中已经实现了23项

在这里插入图片描述
在这里插入图片描述
Code属性

JAVA程序方法体中的代码通过Javac编译器处理以后,最终变为字节码指令存储在Code属性中。

max_stack表明操做栈深度的最大值,在方法执行的任什么时候刻,操做数栈都不会超过这个深度,虚拟机运行的时候须要根据这个值来分配栈帧中的操做栈深度。

max_locals表明局部变量表所需的存储空间,单位为slot,对于byte,char,float,int,short,boolean,reference和returnAddress每一个局部变量占用一个slot,而double和long须要两个slot.

并非方法中用到了多少个局部变量,就把这些局部变量所占的Slot之和做为max_locals的值,缘由是局部变量表中的slot能够重用,当代码执行超过一个局部变量的做用域时,这个局部变量所占用的slot就能够被其余局部变量所使用。这个值编译器会自动计算得出

code_length和code用来存储java源程序编译后生成的字节码指令,code_length表明字节码长度,code用于存储字节码指令的一系列字节流。字节码的每一个指令就是一个字节。这样能够推出,一个字节最多能够表明256条指令,目前已经使用了约200条。而code_length有4个字节,因此一个方法作多容许有65535条字节码指令,若是超过这个限制,javac就会拒绝编译,通常JSP可能会这个缘由致使失败。

在任何实例方法中,均可以经过this关键字访问到此方法所属的对象,它的底层实现就是经过javac编译器在编译的时候把this关键字的访问转变为对一个普通方法参数的访问

Exceptions属性

表示方法可能抛出number_of_exceptions种受查异常,每种受查异常使用一个exception_index_table项表示

LineNumberTable属性

用于描述java源代码行号与字节码行号直接的对应关系。能够用-g:none或-g:lines选项来取消或要求生成这项信息,主要影响是报错时对战是否显示出错的行号。同时debug时没法设置断点。

LocalVariableTable属性

用于描述栈帧中局部变量表中的变量与源码中定义的变量之间的关系。也能够选择开关,关闭后果就是报错时看不到变量名称

SourceFile属性

用于记录生成这个Class文件的源码文件名称。可选

ConstantValue属性

通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可使用这项属性。

在JAVA中,int x=123;和static int x=123;的区别在于,非static类型的变量(实例变量)的赋值是在实例构造器方法中进行的;而对于静态变量,则有两种方式能够选择:在类构造器方法中进行,或者使用ConstantValue属性来赋值。目前SUN JAVAC编译器的选择是若是同时使用final和static来修饰一个变量,而且这个变量的数据类型是基本类型或者String的话,就生成ConstantValue属性来进行初始化,若是这个变量没有用final修饰,或者非以上类型,则选择在中进行初始化。

InnerClasses属性

用于记录内部类与宿主类之间的关系

Deprecated和synthetic属性

都属于标志类布尔属性。deprecated表示在代码中使用@deprecated注释进行设置。synthetic表示字段或方法不是java源码产生,而是编译器自行添加的。

  1. 字节码指令

共有十种指令

  1. 加载和存储指令

用于将数据在栈帧中的局部变量表和操做数栈之间来回传输

  1. 运算指令

用于对两个操做数栈上的值进行某种运算,并把结果从新存入的栈顶,可分为整形数据运算指令与浮点型数据运算指令

  1. 类型转换指令

将两种不一样数值类型的进行相互转换,通常用户实现代码中显式的类型转换操做

  1. 对象的建立于访问指令

java虚拟机对于类的实例与数组的建立与操做使用了不一样的字节码命令

在这里插入图片描述
5. 控制转移指令

可让java虚拟机有条件或者无条件的从指定的位置指令,而不是控制转移指令的下一条

  1. 方法调用和返回指令

方法调用,分派执行过程

  1. 异常处理指令

在java红显示抛出异常状况(throw语句)都是由athrow指令来实现

在java虚拟机中,处理异常(catch语句),不是有字节码指令来实现,而是采用异常表来完成的

  1. 同步指令

java虚拟机能够支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(monitor)来支持的

方法级的同步是隐式的,无须经过字节码指令来控制,它实如今方法调用与返回操做之中,

总结

class文件是java虚拟机执行引擎的数据入口,上面主要介绍了class类文件的各个组成部分,以及每一个部分的定义、数据结构和使用方法。

2. 虚拟机类加载机制

开篇:

这一部分的内容主要介绍class文件如何加装到虚拟机中,及在虚拟机中会发生怎样的变化

虚拟机类加载机制:

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的java类型。

1. 类的加载时机

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载7个阶段

在这里插入图片描述

除解析阶段外,其余几个阶段的顺序都是固定的。解析阶段在某些状况下能够在初始化阶段以后再开始,这是为了支持JAVA语言的运行时绑定(动态绑定/晚期绑定)

虚拟机规范严格规定了有且只有5种状况必须对类进行初始化(加载,验证,准备自动在以前开始)

  1. 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,若是类没有进行初始化,则先初始化。这4个字节码常见的出现场景是:使用new关键字实例化对象的时候,读取或设置静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用时,若是类没有进行初始化,则须要先触发其初始化
  3. 初始化一个类时,若是其父类还未初始化,则先出发父类初始化。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类,虚拟机会先初始化这个主类
  5. 针对jdk1.7,若是java.lang.invoke.MenthodHandle最后解析的结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且这个句柄所对应的类没有进行过初始化,则会触发其初始化
2. 类的加载过程

加载

在加载阶段,虚拟机须要完成如下三件事情:

  1. 经过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构
  3. 在JAVA堆中生成一个表明着各种的java.lang.Class对象,做为方法区这些数据的访问入口

事实上,这三条限定都不是很严格,好比第一条,并无明确指出经过全限定名从哪里获得二进制流,由此就有不少不一样的实现:

  • 在ZIP包中读取(JAR,EAR,WAR)
  • 从网络中获取(APPLET)
  • 运行时计算生成,这种场景使用的最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口申城$Proxy的代理类的二进制流
  • 由其它文件生成(jsp)
  • 从数据库中读取,这种场景比较少见,有些中间件服务器(SAP NETWEAVER)

加载阶段完成后,虚拟机外部的二进制流就按照虚拟机所需的格式存储在方法区中,方法区中的数据存储格式由虚拟机实现自行定义。而后在JAVA堆中实例化一个java.lang.Class类对象,这个对象将做为程序访问方法区中的这些类型数据的外部接口。加载阶段与链接阶段的部份内容是交叉进行的,加载阶段还没有完成,链接阶段可能已经开始。

验证

验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并却不会危害虚拟机自身的安全。

验证阶段是很是重要的,这个阶段是否严谨决定了java虚拟机是否能承受恶意代码的攻击,一些在编译层面上能够控制的事情(好比超边界访问数组,跨类型进行类型对象转换存在时,编译器是拒绝工做的)能够经过直接修改class文件的方式进行破解,这就是验证阶段存在的缘由。

验证阶段分为4个校验动做

  1. 文件格式的验证

好比:验证魔数
主、次版本号
常量池中的常量是否有效且虚拟机支持

  1. 元数据的验证

类是否有父类(除了java.lang.Object)
若是不是抽象类,是否实现或者继承了父类全部的方法

  1. 字节码的验证

保证任意时刻操做数栈的数据类型与指令代码都能配合工做,好比在操做栈放了int类型,结果被long类型加载到本地变量中
保证方法体中的数据类型转换是有效的

  1. 符号引用验证

符号引用中经过字符串描述的全限定名是否能找到对应的类
在指定的类中是否存在符合方法的字段描述
符合引用中类、字段、方法访问性是否可被当前类访问

准备

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个时候内存分配的仅包括类变量(static变量),不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在java堆中。

其次是这里所说的初始值“一般状况下”是数据类型的零值(随后在初始化阶段生成定义的初值)。若是该变量被final修饰,将在编译时生成ConstantValue,这样在准备阶段将直接设置成该初值。

基本类型零值

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用在CLASS文件中它以CONSTANT_CLASS_INFO,CONSTANT_FIELDREF_INTO,CONSTANT_METHODREF_INFO等类型的常量出现。

1.符号引用:(Symbolic References)符号引用以一组符号来描述所引用的目标,能够是任何形式的字面量,引用的目标并不必定已经加载到内存中,与虚拟机内存布局无关。

2.直接引用:(Direct References)直接引用能够是直接指向目标的指针,相对偏移量,或是一个能间接定位到目标的句柄。与虚拟机内存布局相关。

初始化

是类加载过程的最后一步,初始化阶段才真正开始执行类中定义的JAVA程序代码

准备阶段中,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员经过程序制定的计划来赋值或者说,初始化阶段是执行类构造器()方法的过程

3. 类加载器

双亲委派模型

只存在两种不一样的类加载器:启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机自身的一部分。另外一种是全部其余的类加载器,使用JAVA实现,独立于JVM,而且所有继承自抽象类java.lang.ClassLoader.

双亲委派模型要求除了顶层的启动加载类外,其他的类加载器都应当有本身的父类加载器。这里类加载器之间的父子关系通常不会以继承的关系来实现,而是使用组合关系来复用父类加载器的代码。

双亲委派模型的工做过程是:当一个类加载器受到类加载请求,它首先不会本身尝试去加载这个类,而是把这个请求委派给本身的父类加载器去完成,只有当父加载器表示本身没法完成这个加载请求时,子加载器才会尝试本身去加载。

这个模型的好处,就是保证某个范围的类必定是被某个类加载器所加载的,这就保证在程序中同一个类不会被不一样的类加载器加载。这样作的一个主要的考量,就是从安全层面上,杜绝经过使用和JRE相同的类名冒充现有JRE的类达到替换的攻击方式。

类加载双亲委派模型

3. 虚拟机字节码执行引擎

在不一样的虚拟机中,执行引擎在执行java代码的时候可能会有解释执行(经过解释器执行)和编译执行(经过即时编译器产生本地代码执行)

1. 运行时栈帧结构

  栈帧(StackFrame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(VirtualMachine Stack)的栈元素。栈帧存储了方法的局部变量表,操做数栈,动态链接和方法返回地址等信息。每个方法调用的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧概念结构图

局部变量表

  是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,在编译成CLASS文件时,就在方法的CODE属性的max_locals数据项中肯定了该方法所须要分配的最大局部变量表的容量。

操做数栈

操做数栈也称为操做栈,后入先出,在一个方法执行以前操做栈是空的,在方法的执行过程当中,会有各类字节码指令往操做栈中写入和读取内容,也就是入栈、出栈的操做

动态链接

  每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接。字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转换为直接引用,成为静态解析,另外一部分会在每一次运行时转换为直接引用,成为动态链接。

方法返回地址

有两种方式退出当前执行的方法,一是执行引擎遇到任意一个方法返回的字节码指令,这种方法称为正常完成出口。二是在方法执行过程当中遇到没法处理的异常,这种方法称为异常完成出口。

不管哪一种方法,方法退出后,都须要返回到调用者的位置,正常退出时,调用者的PC计数器值能够做为返回地址,栈帧中极可能会保存这个计数器值,而异常退出时,返回地址要经过异常处理器表来肯定。

方法退出的过程其实是将当前栈帧出栈,并恢复上层方法的局部变量表和操做数栈,把返回值压入调用者的操做数栈中。

2. 方法调用

方法调用并不等同于方法执行,其惟一的任务就是肯定调用哪个具体方法。

解析

全部方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,而不是方法在实际运行时内存布局中的入口地址。

在类加载的解析阶段,一部分符号引用会被转化为直接引用,这种解析成立的前提是:方法在程序真正运行以前就有一个可肯定的调用版本,且这个方法的调用版本在运行时是不可改变的(“编译期可知,运行期不可变”)。

分派

静态分派

Parent father =new Son();这句中,Parent被称为静态类型,Son称为实际类型。

虚拟机(编译器)重载时经过参数的静态类型做为判断依据。全部依赖静态类型来定位方法执行版本的分派动做,都称为静态分派。静态分派的最典型应用就是方法重载。

在重载的状况下,不少时候重载版本并非惟一的,而是寻找一个最合适的版本。好比存在多个重载方法的状况中,调用的目标顺序是必定的。

动态分派

它与重写(override)有着密切的关系。

相对于前面的重载中,引用类型对于具体调用哪一个方法起决定性做用,在重写中,引用指向的对象的具体类型决定了调用的具体目标方法。

单分派和多分派

方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,能够将分派划分为单分派和多分派两种:单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。

首先进行静态分派,生成相应的字节码,在常量池中生成对应的方法符号引用,这个过程根据了两个宗量进行选择(接收者和参数),所以静态分派是多分派类型。再进行动态分派,将符号引用变成直接引用时,只对方法的接收者进行选择,所以只有一个宗量,动态分派是单分派。

3. 基于栈的字节码解释执行引擎

  Java编译器输出的指令流,基本上是一种基于栈的指令集架构,与之对应的是寄存器指令集架构。基于栈的指令集主要的优势是可移植性。而寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地受到硬件的约束。