Fork me on GitHub

虚拟机执行子系统

注意:所有文章除特别说明外,转载请注明出处.

虚拟机执行子系统

[TOC]

第六章 类文件结构

程序编译的结果从本地机器码转变为字节码。

提示:实现语言无关性的基础仍然是虚拟机和字节码存储格式。Class文件中包含了Java虚拟机指令集和符号表以及若干其它辅助信息。

6.1 概述

将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

6.2 无关性基石

1.Java刚诞生的口号:一次编写,到处运行(Write Once, Run Anywhere)。

2.实现虚拟机的语言无关性的基础是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,这使得任何语言的都可以使用特定的编译器将其源码编译成Class文件,从而在虚拟机上运行。

6.3 Class类文件的结构

注意:任何一个Class文件都对应着唯一一个类或接口的定义信息。但相反地,类或接口并不一定都得定义在文件里(比如类或接口也可以通过类加载器直接生成)。

Class文件是以一组8位字节为基础单位的二进制流,中间没有分隔符。

Class文件中字节序为Big-Endian,最高位字节在地址最低位、最低位字节在地址最高位
Class文件中只有2种数据类型:无符号数和表。

1.无符号数:属于基本类型。

2.表:是由多个无符号数或者其它表作为数据项构成的复合数据类型。

提示:无论是无符号数还是表,当需要描述同一类型但数量不定的多个线程时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式。这时称这一系列连续的某一类型的数据为某一类型的集合。

提示:Class的结构被完全限定的,不能有任何违背官方规定的操作。

6.3.1 魔数和Class文件版本

每个Class文件的头四个字节称为魔数,用于确定这个文件是否为一个能被虚拟机接受的Class文件。紧接着魔数的4个字节存储的是Class文件的版本号,分为次版本号和主版本号。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后的版本的Class文件。

6.3.2 常量池

常量池在主次版本之后,可以理解为Class文件之中的资源仓库,是Class文件结构中与其它项目关联最多的数据类型。常量池的容量计数是从1开始的,0用于描述“不引用任何一个常量池项目”。

常量池中主要存放两大类常量:字面量和符号引用。

1.字面量:接近Java的常量概念。如:文本字符串 | 声明为final的常量值等。

2.符号引用:符号则引用则属于编译原理方面概念。包括:

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

常量池中每一项常量都是一个表,JDK1.7中一共有14种常量类型。

...

6.3.3 访问标志

访问标志用于标示一些类或接口的访问信息,如Class是类还是接口,是否是public,是否定义完为abstract等。如果是类的话,是否被声明为final等。

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

这三项用于确定类的继承关系,类索引用于确定类的全限定名。父类索引用于确定类的父类的全限定名。接口索引集合用来描述类实现了哪些接口,并按照implements语句的顺序排列。

6.3.5 字段表集合

字段表用于描述接口或类中声明的变量。字段包括类级变量以及实例级变量,但是不包括在方法内部声明的局部变量。

字段包括字段修饰符(用标志位描述,和类的访问标志类似),字段的简单名称,字段和方法的描述符以及属性表集合。

1.简单名称是指没有类型和参数修饰的方法或者字段名称。

2.描述符用于描述字段的数据类型,方法的参数列表和返回值。

3.全限定名。如:类的全限定名

字段表集合不会列出从超类或父接口中继承而来的字段,但可能列出原本Java程序中不存在的字段,如:内部类指向外部类的实例的字段。

6.3.6 方法表集合

方法表集合和字段表集合类似,包括了方法的访问标志、名称索引、描述符索引和属性表集合
方法里的Java代码通过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。

如果父类的方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但有可能会出现由编译器自动添加的方法,如类构造器“”方法和实例构造器“”方法。

Java语言中要重载一个方法,除了要有相同的简单名称之外,还要求必须有与原方法不同的特征签名。特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合。由于返回值不包含在特征签名中,所以Java语言里无法仅仅依靠返回值不同来对已有的方法进行重载。

6.3.7 属性表集合

属性表不要求具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略不认识的属性

1.Code属性

Java程序方法体中的代码通过编译器编译成字节码指令后存储在Code属性内
max_stack代表了操作数栈深度的最大值,虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度。

提示:Slot是虚拟机为局部变量分配内存所使用的最小单位,局部变量表中的Slot可以重用
虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令。

通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数。这样就实现了在任何实例方法里面都能通过this关键字访问到此方法所属的对象。

异常表是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。

2.Exceptions属性

Exceptions属性的作用是列举出方法描述时在throws关键字后面列举的异常。

3.LineNumberTable属性

用于描述Java源码行号和字节码行号(字节码的偏移量)之间的对应关系。

可以在Javac中分别使用-g:none和-g:lines来取消或要求生成这项信息;如果关闭,当抛出异常时,堆栈中将不会显示出错的行号。

4.LocalVariableTable属性

描述栈帧中局部变量中的变量与Java源码中定义的变量之间的关系。

可以在Javac中分别使用-g:none和-g:vars来取消或要求生成这项信息。如果关闭,所有参数的名称都将消失,IDE将会使用诸如arg0、arg1之类的占位符代替原油的参数名。

5.ConstantValue属性

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

虚拟机对非static类型的变量的赋值是在实例构造器方法中进行;而对于类变量则有2种方式,在类构造器方法中或者使用ConstantValue属性。

提示:ConstantValue属性值只能限于基本类型和String。

6.Signure属性

用于记录泛型签名中包含类型变量或参数化类型的类、接口、初始化方法或成员的泛型签名信息。

Java语言的泛型采用的是擦除法实现的伪泛型,在字节码(code属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。

提示:Java的反射API获取的泛型类型的最终数据源就是来自Signure属性。

6.4 字节码指令

Java采用面向操作数栈而不是寄存器的架构。

字节码指令集由于限制了虚拟机操作码的长度为一个字节,所以指令集的字节码总数不可能超过256条。

编译器在编译期或运行期将byte和short类型的数据带符号扩展为相应的int类型的数据,将boolean和char类型的数据零扩展为相应的int类型数据。

虚拟机规范规定在处理整型数据时,只有除法指令和求余指令中当出现除数为零时会导致虚拟机抛出ArithmeticException异常。

Java虚拟机在进行浮点数运算时会采用最接近数舍入模式,把浮点数转换整数时,Java虚拟机使用IEEE 754标准的向零舍入模式;多疑浮点数运算不能用于精确计算。

Java虚拟机处理浮点数运算时,当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作没有明确的数学定义的话,将会使用NaN表示。所有使用NaN值作为操作数的算法操作,结果都会返回NaN。

在对long类型的数值进行比较时,虚拟机采用带符号的比较方式,而对浮点数值进行比较采用无信号比较方式。

Java虚拟机直接支持小范围类型向大范围类型的安全转换,比如int到float,而处理窄化类型转化可能会导致结果产生不同的正负号、不同数量级的情况,很可能会导致数值的精度丢失。

Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出的运行时异常
各种数据类型的比较最后都会转化为int类型的比较操作。

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,2种同步结构都是使用管程(Monitor)来支持。Java虚拟机的指令集中有 monitorenter 和 monitorexit 2条指令来支持synchronized关键字的语义。

第七章 虚拟机类加载机制

虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机最直接使用的Java类型的过程就是虚拟机的类加载机制。

7.1 类加载特性

在Java中,类型的加载和连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提高灵活性,Java中天生可以动态扩展语言特性就是依赖运行期动态加载和动态连接实现。

7.2 类加载过程

类的加载过程,从类被加载到虚拟机内存开始,到卸载出内存为止,其整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。

7.2.1 加载

加载是类加载过程的一个阶段,在加载阶段需要完成:

1.通过一个类的全限定名来获取定义此类的二进制字节流。

2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据接口。

3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

7.2.2 验证

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

虚拟机如果不检查输入的字节流,并对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。这个阶段是否严谨,直接决定了java虚拟机是否能承受恶意代码的攻击。

从整体上看,验证阶段大致上会完成4个阶段的校验工作:文件格式、元数据、字节码、符号引用。

1.文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

可能包括的验证点:1.是否以魔数0xCAFEBABE开头。2.主次版本号是否在当前虚拟机处理范围之内。3.常量池的常量中是否有不被支持的常量类型。4.指向常量的各种索引值中是否有指向不存在常量或不符合类型的常量。5. …

2.元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范。

可能包括验证点:1.这个类是否有父类。2.这个类的父类是否继承了不允许被继承的类。3. …

3.字节码验证

该阶段的验证主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。在第二阶段对元数据信息中的数据类型做完检查之后,这个阶段将对类的方法进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。

4.符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证的目的是确保解析动作能正常执行。

可能验证的内容有:1.符号引用中通过字符串描述的全限定名是否能找到对应的类。2.在指定类中是否存在符号方法的字段描述及简单名称所描述的方法和字段。…

7.2.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值阶段,变量所使用的内存都将在方法区中进行分配。

提示:此时进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量,实例变量将在对象实例化时随着对象一起分配在Java堆中。

7.2.4 解析

该阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用和直接引用的关联:

1.概念

1. 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是符合约定的任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

2. 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,引用的目标必定已经在内存中存在。

注意:虚拟机规范没有规定解析阶段发生的具体时间,虚拟机实现可以根据需要来判断到底是在类被加载时解析还是等到一个符号引用将要被使用前采取解析。

2.对解析结果进行缓存

同一符号引用进行多次解析请求是很常见的,除invokedynamic指令以外,虚拟机实现可以对第一次解析结果进行缓存,来避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个引用符号之前已经被成功解析过,那么后续的引用解析请求就应当一直成功。同样的,如果第一次解析失败,那么其他指令对这个符号的解析请求也应该收到相同的异常。

3.解析动作的目标

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限制符7类符号引用进行。前面四种引用的解析过程,对于后面三种,与JDK1.7新增的动态语言支持息息相关,由于java语言是一门静态类型语言,因此没有介绍invokedynamic指令的语义之前,没有办法将他们和现在的java语言对应上。

7.2.5 初始化

类初始化阶段是类加载的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(或者说是字节码)。

7.4 类加载器

类加载器,表示将“通过一个类的全限定名来获取描述此类的二进制字节流”动作放在Java虚拟机外面去实现,从而使得应用程序能够自己觉得如何获取所需要的类。

7.4.1 类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。如果两个类来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等。

7.4.2 类加载器

从Java虚拟机的角度分为两种不同的类加载器:启动类加载器(Bootstrap ClassLoader)和其他类加载器。

1.启动类加载器,使用C++语言实现,是虚拟机自身的一部分。

2.其余的类加载器都由Java语言实现,独立于虚拟机之外,并且全都继承自java.lang.ClassLoader类。(这里只限于HotSpot虚拟机)。

从Java开发人员的角度来看,绝大部分Java程序都会使用到以下3种系统提供的类加载器。

1.启动类加载器(Bootstrap ClassLoader)

这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。

2.扩展类加载器(Extension ClassLoader)

这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3.应用程序类加载器(Application ClassLoader)

这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。

7.4.3 双亲委派机制

双亲委派机制模型:要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里父子关系通常是子类通过组合关系而不是继承关系来复用父加载器的代码。

双亲委派模型的工作过程:1.如果一个类加载器收到了类加载的请求,先将这个请求委派给父类加载器去完成(所以所有的加载请求最终都会传送到顶层的启动类加载器中),只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。

使用双亲委派模型来组织类加载器之间的关系,显而易见的优势:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

7.4.4 破坏双亲委派模型

1.第一次被破坏

1.第二次被破坏

1.第三次被破坏

第八章 虚拟机字节码执行引擎

8.1 概述

==执行引擎是Java虚拟机最核心的组成部件之一==。虚拟机的执行引擎由自己实现,所以可以自行定制指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

从外观上看,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。本节将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。

8.2 运行时栈帧结构

栈帧是用于==支持虚拟机方法调用和方法执行的数据结构==,它是虚拟机运行时数据区中虚拟机栈的栈元素。

栈帧存储了方法的==局部变量表、操作数栈、动态连接和方法返回地址==等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

提示:执行引擎运行所有字节码指令都只针对当前栈帧进行操作。

8.2.1 局部变量表

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

局部变量表的容量以变量槽(Variable Slot)为最小单位。 一个Slot可以存放一个32位以内(boolean、byte、char、short、int、float、reference和returnAddress)的数据类型,reference类型表示一个对象实例的引用,returnAddress已经很少见了,可以忽略。

对于64位的数据类型(Java语言中明确的64位数据类型只有long和double),虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。

虚拟机通过==索引定位的方式使用局部变量表==,索引值的范围从0开始至局部变量表最大的Slot数量。访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,==如果是64位数据类型,就代表会同时使用n和n+1这两个Slot==。

为了节省栈帧空间,局部变量Slot可以重用,方法体中定义的变量,其作用域并不一定会覆盖整个方法体。如果当前字节码PC计数器的值超出了某个变量的作用域,那么这个变量的Slot就可以交给其他变量使用。这样的设计会带来一些额外的副作用,比如:在某些情况下,Slot的复用会直接影响到系统的收集行为。

8.2.2 操作数栈

操作数栈又称为操作栈,它是一个后入先出栈。当一个方法执行开始时,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是 出栈/入栈操作。

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

8.2.3 动态连接

每个==栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用==,持有这个引用是为了支持方法调用过程中的动态连接;

字节码中方法调用指令是以常量池中的指向方法的符号引用为参数的,有一部分符号引用会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为 静态解析,另外一部分在每次的运行期间转化为直接引用,这部分称为动态连接。

8.2.4 方法返回地址

当一个方法执行之后,只有两种方式可以退出这个方法。

1.执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。

2.另外一种是在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理(即本方法异常处理表中没有匹配的异常处理器),就会导致方法退出,这种退出方式称为异常完成出口。

注意:这种退出方式不会给上层调用者产生任何返回值。

无论采用何种退出方式,在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。

一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

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

8.2.5 附加信息

虚拟机规范允许虚拟机实现向栈帧中添加一些自定义的附加信息,例如与调试相关的信息等。

8.3 方法调用

方法调用并不等同于方法的执行,方法调用阶段唯一的任务就是确定被调用方法的版本(调用哪一个方法),不涉及方法内部的具体运行过程,在程序运行时进行方法调用是最普遍频繁的操作。

提示:Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。

8.3.1 解析

“编译期可知,运行期不可变”的方法(静态方法和私有方法),在类加载的解析阶段,会将其符号引用转化为直接引用(入口地址)。这类方法的调用称为“解析(Resolution)”。

在Java虚拟机中提供了5条方法调用字节码指令:

1.invokestatic:调用静态方法

2.invokespecial:调用实例构造器方法、私有方法、父类方法

3.invokevirtual:调用所有的虚方法

4.invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象

5.invokedynamic:先在运行时动态解析出点限定符所引用的方法,然后再执行该方法,在此之前的4条调用命令的分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的

8.3.2 分派

分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在Java虚拟中是如何实现的。

1.静态分派

所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派发生在编译阶段。静态分派最典型的应用就是方法重载。

package Demo.controller;

public class staticDispatch {
    static abstract class Human{}

    static class man extends Human{}

    static class woman extends Human{}

    public void sayhello(Human guy){
        System.out.println("human guy");
    }

    public void sayhello(man guy){
        System.out.println("man guy");
    }

    public void sayhello(woman guy){
        System.out.println("woman guy");
    }

    public static void main(String[] args) {
        Human man = new man();
        Human woman = new woman();
        staticDispatch staticDispatch = new staticDispatch();
        staticDispatch.sayhello(man);

        staticDispatch.sayhello(woman);
    }
}

总结:Human man = new Man();其中的Human称为变量的静态类型,man称为变量的实际类型。那么两者的区别在于:静态类型在编译期可知,而实际类型到运行期才能确定。在重载时通过参数的静态类型而不是实际类型作为判定依据,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

所以选择了sayhello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。

2.动态分派

在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。最典型的应用就是方法重写。

3.单分派和多分派

方法的接收者、方法的参数都可以称为方法的宗量。根据分批基于多少种宗量,可以将分派划分为单分派和多分派。单分派是根据一个宗量对目标方法进行选择的,多分派是根据多于一个的宗量对目标方法进行选择的。

Java在进行静态分派时,选择目标方法要依据两点:一是变量的静态类型是哪个类型,二是方法参数是什么类型。因为要根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。

运行时阶段的动态分派过程,由于编译器已经确定了目标方法的签名(包括方法参数),运行时虚拟机只需要确定方法的接收者的实际类型,就可以分派。因为是根据一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。

注:到JDK1.7时,Java语言还是静态多分派、动态单分派的语言,未来有可能支持动态多分派。

4.虚拟机动态分派的实现

由于动态分派是非常频繁的动作,而动态分派在方法版本选择过程中又需要在方法元数据中搜索合适的目标方法,虚拟机实现出于性能的考虑,通常不直接进行如此频繁的搜索,而是采用优化方法。

其中一种“稳定优化”手段是:在类的方法区中建立一个虚方法表(Virtual Method Table, 也称vtable, 与此对应,也存在接口方法表——Interface Method Table,也称itable)。使用虚方法表索引来代替元数据查找以提高性能。其原理与C++的虚函数表类似。

虚方法表中存放的是各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类中该方法相同,都指向父类的实现入口。虚方法表一般在类加载的连接阶段进行初始化。

8.3.3 动态类型语言支持

JDK新增加了invokedynamic指令来是实现“动态类型语言”。

静态语言和动态语言的区别:

静态语言(强类型语言): 静态语言是在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据类型。  例如:C++、Java、Delphi、C#等。

动态语言(弱类型语言) : 动态语言是在运行时确定数据类型的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。  例如PHP/ASP/Ruby/Python/Perl/ABAP/SQL/JavaScript/Unix Shell等等。

强类型定义语言 : 强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。举个例子:如果你定义了一个整型变量a,那么程序根本不可能将a当作字符串类型处理。强类型定义语言是类型安全的语言。

弱类型定义语言 : 数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。强类型定义语言在速度上可能略逊色于弱类型定义语言,但是强类型定义语言带来的严谨性能够有效的避免许多错误。

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

虚拟机如何调用方法的内容已经讲解完毕,现在我们来探讨虚拟机是如何执行方法中的字节码指令。

8.4.1 解释执行

Java语言经常被人们定位为“解释执行”语言,在Java初生的JDK1.0时代,这种定义还比较准确的,但当主流的虚拟机中都包含了即时编译后,Class文件中的代码到底会被解释执行还是编译执行,就成了只有虚拟机自己才能准确判断的事情。再后来,Java也发展出来了直接生成本地代码的编译器[如何GCJ(GNU Compiler for the Java)],而C/C++也出现了通过解释器执行的版本(如CINT),这时候再笼统的说“解释执行”,对于整个Java语言来说就成了几乎没有任何意义的概念,只有确定了谈论对象是某种具体的Java实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。

Java语言中,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程,因为这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机内部,所以Java程序的编译就是半独立实现的,

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

Java编译器输出的指令流,基本上是一种基于栈的指令集架构(Instruction Set Architecture,ISA),依赖操作数栈进行工作。与之相对应的另一套常用的指令集架构是基于寄存器的指令集, 依赖寄存器进行工作。

基于栈的指令集主要的优点就是可移植,寄存器是由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。

栈架构的指令集还有一些其他的优点,如代码相对更加紧凑,编译器实现更加简单等。
栈架构指令集的主要缺点是执行速度相对来说会稍微慢一些。

第九章 类加载及执行子系统的案例实战

9.1 Tomcat:正统的类加载架构

本文标题:虚拟机执行子系统

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:19:29

原始链接:http://bangjinhu.github.io/undefined/虚拟机执行子系统/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.