Java虚拟机 虚拟机执行子系统

代码编译的结构从本地机器码转变为字节码,是储存格式发展的一小步,倒是编程语言发展的一大步。java

主要内容

类文件结构编程

虚拟机类加载机制数组

虚拟机字节码执行引擎安全

类文件结构

无关性基石

各类不一样的虚拟机均可以载入和执行一种平台无关的字节码,从而实现“一次编写,处处运行”。数据结构

语言无关性愈来愈受到开发者重视,java只是运行在java虚拟机上的一种语言。多线程

实现语言无关性的基础是虚拟机和字节码储存格式,使用java编译器能够把java编译成储存字节码的class文件,其余语言也能够把代码编译成class文件,虚拟机只是执行class文件而不关心文件来源。架构

Java中各类变量、关键字和运算符的最终语义都是由多条字节码组成的,所以字节码的语义描述能力强于java语言自己,这也为其余语言实现有别于java的语言特性提供了基础。编程语言

class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序储存在class文件中,中间没有任何间隔,因此这些都是必要数据。编辑器

这种数据结构中只有两种类型的数据:无符号数和表。布局

无符号数是基本数据类型,能够描述数字、索引引用、数量值或者utf-8编码的字符串。

表是由多个无符号数或者表组成的混合数据,整个class文件本质上就是一张表。

魔数class文件的版本

Class文件前4个字符是魔数,惟一做用是用来肯定这个文件是否能够被虚拟机接收。

魔数后的4个字符是class文件版本号,5/6为次版本,7/8为主版本。高版本的JDK能兼容低版本的class文件,反之不能够。

常量池

主次版本号以后就是常量池入口,常量池是class文件中与其余项目关联最多的数据类型。

常量池主要存放字面量和符号引用。字面量比较像java语言层面的常量概念,好比字符串,定义为final的变量;符号引用则属于编译原理方面的概念,包括下面3类常量:

类和接口的全限定名;字段的定义和描述符;方法的定义和描述符。

Java代码进行javac编译时不会保存各个方法和字段的最终布局信息,虚拟机运行时,须要从常量池获取对应的符号引用,再在类建立或运行时解析并翻译到具体内存地址中。

因为class文件中方法、字段等须要引用CONSTANT_Utf8_info型常量描述,因此他的最大长度就是方法和字段名的最大长度。最大长度为length的最大值,即u2类型最大值65535。

访问标志

常量池以后是2字节的访问标志,表示类和接口层次的访问信息,好比是否为接口,是否public,是否抽象等。

类索引、父类索引和接口索引集合

类索引和父类索引是u2类型数据,接口索引集合是一组u2数据集合,class文件由这三个数据肯定继承关系。

字段表集合

字段表用于描述接口或类中定义的变量。字段包括了类级别变量或实例级变量,不包括方法内部的变量。描述一个字段的信息,好比做用域、数据类型等各个修饰符都是布尔值。而字段名等没法固定的,只能引用常量池中的常量描述。

方法表集合

与字段表集合相似。

方法里的代码,通过编辑器编译成字节码指令以后存放在方法表集合属性中“code”的属性里。

Java中重载一个方法,须要有一个与原来方法不一样的特征签名,他就是一个方法中各个参数在常量池中的字符引用集合,可是返回值不包含在特征签名中,因此没法用返回值不一样重载方法。可是在class文件中特征签名包括了返回值,即方法返回值不一样就能够同时存在,其余语言能够利用这个特性。

属性表集合

Class文件、字段表、方法表均可以携带本身的属性表集合,以描述某些场景专有信息。

不像class文件其余项目同样有严格的数据限制。其余人实现的编译器能够像属性表中添加自定义属性,虚拟机会忽略不认识的属性。

虚拟机类加载机制

类加载时机

按照这个顺序开始加载,各个阶段能够重叠。解析和使用顺序不固定。

1.遇到new、getstatic、putstatic或invokestatic这4个指令时,若是类没初始化,则须要先触发初始化;

2.使用java.lang.reflect包的方法对类进行发射调用时;

3.初始化类时,若是父类没初始化则先初始化父类;

4.虚拟机启动时,须要先初始化执行主类(包含main()方法那个类);

这四种为主动引用,其他都是被动引用。

经过子类引用父类中的静态字段,只会初始化定义这个字段的类(父类)。

经过数组定义引用类,不会触发初始化。

常量在编译阶段会存入调用类的常量池中,本质上没直接引用定义常量的类,因此不会加载定义常量的类。

接口中不能使用“static{}”语句块,可是编译器仍然会生成类构造器用于初始化成员变量。接口只有在真正用到父类接口时才会初始化父接口,其他主动初始化与类一致。

类加载的过程

加载:

1.      经过类的全限定名获取此类的二进制字节流。

2.      将这个字节流的静态储存结构转换为在方法区的运行时数据。

3.      在堆内存中生成表明这个类的java.lang.class文件,做为访问这些数据的入口。

验证:

保证class文件中的字节流数据符合虚拟机的要求,不会对虚拟机形成安全问题。

验证的四个阶段:文件格式、元数据、字节码和符号引用。

验证机制很是重要但不是必要的,能够关闭验证以缩短类加载时间。

准备:

正式为类变量分配内存并设置初始值,这些内存都在方法区中进行分配。

仅仅分配类变量(static)而不包括实例变量,实例变量将在初始化时在堆内存中分配。其次这里说的初始化“一般状况下”是数据的零值。好比:

Public static int value = 123;

准备阶段事后的初始值是零值(0),在初始化时才会赋值为123。而加上final后准备阶段可直接赋值为123。

解析:

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

初始化:

在类加载过程当中,除了加载阶段能够用自定义类加载器以外,其余过程都有虚拟机主导和控制,这一阶段才真正开始执行类中定义的java程序代码(字节码)。

初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中全部类变量的赋值动做和静态语句块中的语句合并产生的。与构造方法不一样,他不须要显示的调用父类构造器,而且虚拟机保证子类<clinit>()方法执行前,父类<clinit>()方法已经执行完毕。

<clinit>()方法不是必须的,若是一个类没有静态语句块也没有赋值操做,那么编译器能够不为这个类生成<clinit>()方法。

虚拟机会保证<clinit>()方法在多线程中正确的加锁和同步。

类加载器

类加载阶段中的“经过全限定名称来获取二进制字节流”动做放到虚拟机外实现,以便让程序本身决定如何去获取所须要的类。实现这个动做的代码块称为类加载器。

类与类加载器

对于任意一个类,都须要这个类的加载器与类自身肯定其在虚拟机中的惟一性。比较两个类是否“相等”,须要在同一个类加载器的前提下才有意义。

双亲委派模型

在虚拟机的角度只存在两种类加载器,一种是启动类加载器,用C++实现并是虚拟机的一部分。另外一种就是其余类加载器,用java实现而且独立于虚拟机以外,都继承自抽象类java.lang.ClassLoader。

双亲委派模型的工做过程:类加载器收到加载一个类的请求时,会先把请求委派给父类加载器,每层都是如此,最终都会传到顶层的启动类加载器,当父类没法完成加载请求(找不到类)时,子类加载器才会尝试本身加载。

双亲委派模型的好处是,类随着他的类加载器一同具备了优先级的层次关系,对于保证程序稳定很重要。好比java.lang.Object类,他存放在rt.jar中,不管哪一个加载器要加载他,最终都会委派为启动类加载器,保证Object类在各类类加载器环境中都是同一个类。

双亲委派模型不是强制的,只是设计者们推荐的模型。

虚拟机字节码执行引擎

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和执行的数据结构,是虚拟机运行时数据区的虚拟机栈中的栈元素。栈帧储存了方法的局部变量表,操做数栈,动态连接和方法返回地址等信息。每个方法从执行到完成,对应着一个栈帧从入栈到出栈的过程。

在程序编译代码的时候,栈帧中须要多大的局部变量表、多深的操做数栈已经肯定并写入到方法表的code属性中,不会在运行期受数据变量的影响,仅仅取决于虚拟机的实现。

局部变量表

局部变量表是一组变量值储存空间,用于存放方法参数和方法内定义的局部变量。

局部变量表的容量以变量槽(variable slot)为最小单位,一个slot能够存下32位如下的数据类型,其中包括了reference(对象的引用)和returnAddress。虚拟机至少(不明确)能够今后引用中查询出对象在堆内存中的起始地址索引和方法区中对象的类型参数。ReturnAddress指向了一条字节码指令的地址。

Slot是可重用的,方法体中定义的变量不必定贯穿整个方法,若是当前字节码PC计数器的值已经超出了某个变量的做用域,slot能够交给其余变量使用,这样不只能够节省栈空间,还能够在必定程度上直接影响垃圾回收。好比:当离开了一个变量的做用域以后,没有任何对局部变量的读写操做,变量原先被占用的slot没有被其余变量复用,因此做为GC Roors的一部分的局部变量表仍然保持着对它的关联(不会被回收)。可是若是一个方法后面有一些很耗时的操做,并且定义了占用大量内存、实际上不会再使用变量,手动将其设置为null就不是一个毫无心义的做用。不过不该当对赋null过多依赖,以恰当的做用域控制回收时间才是优雅的方案。

操做数栈

它是一个后入先出的栈,它的每个元素能够是任意数据类型。当一个方法刚开始执行时,操做栈是空的,执行过程当中会有各类字节码指令提取和写入内容。

方法返回值

方法执行后有两个方式退出:一个是遇到返回字节码指令,另外一个是异常退出。退出后都须要返回到方法被调用的位置,并可能须要在栈帧中保存一些信息来帮助上层方法恢复执行状态。退出过程至关于把当前栈帧出栈。

方法调用

不等同于方法执行,惟一任务就是肯定被调用方法的版本(即调用哪一个方法),暂时不涉及方法内部运行过程。

解析:

全部方法调用中的目标方法在class文件中都是一个常量池的符号引用,在类加载的解析阶段,会把一部分符号引用转为直接引用,前提是方法在程序调用以前就有一个可用版本,而且在运行期是不可变的。

分派:

1.      静态分派

Human man = new Man();

上面的Human称为变量的静态类型,Man称为实际类型。两种类型在程序中均可以发生变化,区别是静态类型变化仅仅在使用时发生,变量自己的静态类型不会改变,编译期可知的;实际类型变化的结果在运行期才可肯定,编译器在编译期并不知道一个对象的实际类型是什么。

全部依赖静态类型来定位方法执行版本的分派动做,都称为静态分派。最典型的应用是方法重载。发生在编译阶段,即不是虚拟机来执行的。字面量没有显示的静态类型,因此重载时每每只能肯定一个“更适合的类型”。

2.      动态分派

和重写有密切关联。重写方法的两条调用指令,不管是指令(都是invokeVirtual)仍是参数(常量池中的常量)都同样,只是目标方法不一样,缘由是invokeVirtual指令第一步就是在运行期肯定接收者的实际类型,因此两次调用中invokeVirtual指令把常量池中类方法符号引用解析到了不一样的直接引用上,这就是重写的本质。把运行期根据实际类型肯定方法执行版本的分派过程称为动态分派。

3.      单分派与多分派

编译阶段编译器的选择过程,即静态分派过程,是根据静态类型和方法参数两个宗量,重载方法的不一样参数指向不一样方法的符号引用,是根据多个宗量进行选择即静态分派属于多分派类型。

运行阶段虚拟机的选择,即动态分派过程,在执行代码对应的invokeVirtual指令时,因为编译期已经决定了目标方法的签名,参数的静态类型、实际类型都不会对方法的选择构成影响,惟一能够影响虚拟机选择的因素只有此方法的接收者实际类型。只有一个宗量做为选择依据,即动态分派属于单分派类型。

java1.6是静态多分派、动态单分派的语言,可是不表明之后不会改变。

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

许多java虚拟机在执行java代码时都有解释执行(解释器执行)和编译执行(经过即时编译器编译成本地代码)两种执行方式。

解释执行

许多高级虚拟机语言,大多都遵循这种基于现代经典编译原理的思路,执行前先对源码进行词法和语法分析处理,把源码转化为抽象语法树。

对于一门语言,能够把几乎所有编译过程独立执行引擎,造成一个完整意义的编译器去实现,好比C/C++语言;也能够把其中一部分(抽象语法树以前)实现为一个半独立编译器,好比java;又能够把这些编译步骤和执行引擎所有封装在一个黑匣子里,好比大多数JavaScript执行器。

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

Java编译器输出的指令流,基本上是一种基于栈的指令集架构。

二者区别:

分别使用两种指令集计算“1+1”。

基于栈的指令集:把两个常量1压入栈,而后取出栈顶2个常量相加,返回结果放回栈顶,最后把栈顶的值放入局部变量第0个slot。

基于寄存器的指令集:把一个寄存器的值设为1,而后使值增长1,结果还保存在这个寄存器中。

基于栈的指令集可移植性好,不像寄存器同样由硬件直接提供,缺点是执行稍慢。