深入理解jvm--字节码引擎(没人看就潦草更~_~)

提纲

运行时栈帧结构

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

对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。


局部变量表

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

局部变量表的容量表以变量槽Slot为最小单位。每个Slot都应存放一个32位以内的数据类型,java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference和returnAddress8种类型。

对于64位数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。Java中明确的64位数据类型只有long和double两种。reference类型可能是32位也可能是64位。由于局部变量表建立在线程的堆栈上,是线程私有的数据,所以不会引起数据安全问题。

为了节省栈帧空间,局部变量表中的slot是可以重用的,方法体中定义的变量,其作用与并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域之,那么这个变量对应的slot就可以交给其它变量使用。

如果遇到了一个方法,其后面的代码有一些耗时很长的操作,而前面有定义了占用大量内存、实际上已经不会再使用的变量,手动将其设置为null值可以将当前变量占用的slot进行垃圾回收交给其他变量使用。

类变量有两次赋初值的阶段:一次是准备阶段,赋给系统初始值;一次是初始化阶段,赋予用户定义值。而局部变量定义不赋初始值不能使用。

操作数栈

是一个后入先出栈。操作数栈中的元素必须与字节码指令的序列严格匹配。

Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,栈即操作数栈。

动态连接

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

方法返回地址

方法推出的两种方式:

正常完成出口:执行引擎遇到任意一个方法返回的字节码指令。

异常完成出口:在方法中遇到了异常,并且这个异常没有在方法体内得到处理。即本方法的异常表中没有搜索到匹配的异常处理器,导致方法退出。

附加信息

规范里没有描述的信息,如与调试相关的信息

方法调用

确定被调用方法的版本。

解析

将符号引用转换位直接引用。

编译器可知,运行其不可变的方法:静态方法和私有方法。

方法调用字节码指令:

  invokestatic:调用静态方法。

  invokespecial:调用实例构造器<init>()方法、私有方法和父类方法;

  invokevirtual:调用所有虚方法;

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

  invokedynamic:调用点限定符所引用的方法,然后在执行该方法。其分派逻辑由用户所设定的引导方法决定。

java中的静态方法、私有方法、实例构造器、父类方法在类加载的时候就会吧符号引用解析为该方法的直接引用。这些方法包括final修饰的方法被称为非虚方法,其余的称为虚方法。

分派

java具备面向对象的三个基本特征:继承、封装和多态。分派调用过程将会揭示多态性特征的一些最基本的体现。如“重载”和“重写”在java虚拟机之中是如何实现。

静态分派

Human man = new Man();

Human称为变量的静态类型,或者叫做外观类型。后面的man则称为变量的实际类型。

静态类型不会改变,并且最终的静态类型实在编译器可知的;

实际类型运行期在可确定,编译器不可知。

虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据。

所有以来静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。

编译器虽然能确定出方法的重载版本,但在很情况下这个重载版本并不是唯一的,往往只能确定一个更加合适的版本(字面量的自动转型)。这是因为字面量不需要定义,所以字面量没有显式的静态类型。

动态分派

invokevirtual指令的多态查找过程:

1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C;

2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.illegalAccessError异常。

3. 否则,按照继承关系从上往下依次对C的各个父类进行第二步的搜索和校验过程。

4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

由于invokevirtual指令吧常量池中的类方法符号医用解析到了不同的直接引用上,这个过程就是java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法只能够版本的分派过程称为动态分派。

单分派和多分派

方法的接收者与方法的参数统称为方法的宗量。

根据分派基于多少种宗量,可以将分派分为单分派和多分派。

单分派是指根据一个宗量对目标方法进行选择,多分派则是根据多余一个宗量对目标进行选择。

静态分派取决于参数的静态类型、实际类型。动态方法取决于此方法的接收者的实际类型。

java语言是一门静态多分派,动态单分派的语言。

动态分派的实现

动态分派是非常复杂的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此最常用的稳定优化手段就是为类在方法区建立一个虚方法表,使用虚方法表索引来代替元数据查找以提高性能。

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中mei有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。

动态类型语言支持

动态类型语言的关键特征是它的类型检查的主体过程是在运行器而不是在编译器。提高开发效率。

静态类型语言的特征时类型检查的主题过程是在编译期。利于稳定性及代码达到更大规模。

java.lang.invoke包

这个包的主要目的是在之前靠单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为methodhandle。拥有method handle之后,java语言也就拥有了类似于函数指针或者委托的方法别名的工具了。

Methodhandle与reflection的区别:

  1.reflection是在模拟java代码层次的方法调用,而method handle是在模拟字节码层次的方法调用。在methodhandles.lookup中的3个方法—findstatic、findvirtual、findspecial正是为了对应于invokestatic、invokevirtual&invokeinterface和invokespecial这几条字节码指令的执行校验行为。

  2.reflection中的java.lang.reflect.Method是方法在java一段的全面映像。而method handle仅仅包含预知性该方法相关的信息。

  3.理论上虚拟机在字节码的方法调用指令做的各种优化,在methodhandle上也采用类似思路去支持。而通过反射去调用方法则不行。

invokedynamic指令

目的:为了解决原有4条invoke*指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户有更高的自由度。

invokedynamic指令与前面4条invoke*指令的最大差别就是它的分派逻辑不是由虚拟机内部决定的,而是有程序员决定。

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

解释执行

只有确定了谈论对象是某种具体的java实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。

java语言中,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。

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

计算1+1:
基于栈的指令集:iconst_1

                iconst_1

                iadd

                istore_0

基于寄存器的指令集:mov eax, 1

                    add eax, 1

基于栈的指令集优点是可移植、代码相对更加紧凑、编译器实现更加简单等。

栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。因为频繁的出栈、入栈操作产生指令,并且产生了频繁的内存访问。

基于栈的解释器执行过程

算数运算其实就是将变量在局部变量表与操作数栈之间进行入栈和出栈操作,完成算术运算。


猜你喜欢

转载自blog.csdn.net/yinweicheng/article/details/80918187