深入理解Java虚拟机----(七)字节码执行引擎

 2019-12-22 11:08  阅读(1048)
文章分类:JVM

字节码执行引擎是执行引擎是最重要的一部分,概念模型的总体外观是一致的:输入字节码,过程是字节码解析的等效过程,输出结果。不同的虚拟机有不同的具体实现,大体有解释执行和编译执行两种选择。

运行时栈结构:
栈桢在虚拟机栈中,是支持方法调用和执行的结构。存储局部变量表、操作数栈、动态链接和方法返回地址等。在编译时,需要多大的局部表量表,多深的操作数栈都确定了,所以需要多大内存也确定了。只有处于栈顶部的“当前栈桢”,才是有效的。

  • 局部变量表:存放方法参数和局部变量。以变量槽存放数据,变量槽有索引,从0开始。0是当前对象的引用,用this关键字可以获取。其余是其他参数和局部变量,虚拟机通过索引取值。局部变量不像类变量,不会赋初始值,没赋值过就不能使用。
  • 操作数栈:方法进入时是空的,许多指令会不停的吧数据出入栈。算数指令会对栈内数据计算,写回结果;方法调用会用操作数栈传递参数。
  • 动态链接:指向运行时常量池中,该栈桢所属方法的引用。为了支持方法调用过程中的动态链接。
  • 方法返回地址:方法退出时,需要回到上层调用的位置。栈桢需要保存这个地址。

方法调用:
方法的调用在class文件里,只是符号引用的形式。直到类加载阶段甚至运行时,才能确定内存入口–直接引用。

  • 解析:在类加载的解析阶段,将一部分的符号引用转换为直接引用,前提是运行前有一个确定版本,而且不可变,这种叫解析。主要包括:静态方法、私有方法、实例构造器、父类方法。这是一个静态的过程。

  • 分派:分派可能是静态,也可能是动态。根据宗量数可分为单分派和多分派。于是就有4种可能。

    • 静态分派:静态分派发生在编译阶段,典型应用是重载。重载是通过参数的外观类型来分派的。下面这段代码会输出什么呢?答案是“h”。因为H是h的外观类型或者叫静态类型。静态类型是编译期可知的,而h的实际类型却要使用时可知,因为运行时可变的。所以编译期能确定的最接近的类型是H。根据这个类型,将第一个say方法的符号引用作为了调用方法指令invokevirtual的参数。

2019120001593\_1.png

  • 动态分派:和重写有很大关系。invokevirtual的过程大概是,找到找到操作数栈顶第一个元素的实际类型C。在C中查找与常量中完全相符的方法,如果找不到就层层往上,查找父类。这个过程第一步就是找实际类型,然后直接找方法执行。这就是重写的本质。这种运行时动态的确定执行版本的过程,称为动态分派。
  • 单分派、多分派:方法的接受者和参数,总称为方法的宗量。单分派就是根据一个宗量对方法进行选择。多分派当然就是根据多个宗量选择。
  • 静态分派需要根据静态类型和参数确定,所以是多分派的。而动态分派已经确定了参数信息,只需确定方法接收者的实际类型。所以动态分派是单分派。由此可见,java是一门静态多分派、动态单分派的语言。

动态类型语言支持:
动态语言的特征是,它的类型检查过程是在运行期间,而不是编译期。例如lua、Python。在编译器检查就是静态语言,如c++、java。一门语言中每一种检查要在编译期检查还是运行期检查,没有必然的逻辑因果关系,都是规范中人为规定的。在java中,编译期酒将方法调用翻译成了符号引用,这个符号引用包括了这个方法属于哪个类型、方法名、参数等信息;而在动态语言中
变量是没有类型的,只有变量的值才有类型,编译时只能确定方法名、参数等,方法的接受者不固定,要在运行时才知道。 静态更安全,编译时发现问题,扩大代码规模。动态更灵活,代码精巧、提升效率。 JDK1.7以前的指令集,4种方法调用指令,第一个参数都是被调用方法的符号引用。但符号引用是在编译期产生,而动态语言中方法的接受者在运行时才确定。这样,在JVM上实现动态语言需要用其他复杂的方式。在JDK1.7中加入了invokedynamic指令和java.lang.invoke包。这两种方式都是为了解决原有的调用指令规则固化到虚拟机中的问题,将方法的查找由虚拟机交到了用户代码手中。前者用字节码和类属性、常量实现;后者用java API实现。 java.lang.invoke中提供了MehtodHandler,类似于对一个方法的引用,比如c++的函数指针,c#的delegate。有了这个引用,就可以实现对方法的调用。有人会疑问,这个通过反射不是早都实现了么?他们之间还是有很大的差别:

  • 反射是模拟Java代码层面的方法调用,MehtodHandler是字节码层面的。MehtodHandler中的几个find方法对应着几个方法调用指令。
  • 反射中,Method包含了方法各种各样的复杂信息,是重量级的。而MehtodHandler只包含执行该方法的相关信息,是轻量级的。
  • MehtodHandler是对字节码指令的模拟,虚拟机的优化可以应用到上面,反射则不行。
  • 反射是为了Java服务的,而MehtodHandler是为了虚拟机服务,可以支持虚拟机上的多种语言。

invokedynamic指令出现的位置叫动态调用点,这个指令的第一个参数不再是以前的符号引用,而是JDK1.7新加入的常量,包含:引导方法、方法类型、名称。 一个例子,有祖父类A,包含方法F,父类B继承A,重写F,子类C继承B,重写F。如何在C的F方法中调用B和C中的F方法?B中的F方法可以通过super轻易调用,那A中的呢?单纯的java代码是做不到的。可以用MehtodHandler实现。

基于栈的字节码解释执行引擎
JVM在执行Java代码时,有两种选择:解释执行、编译执行。
解释执行:
2019120001593\_2.png
现代基于虚拟机的高级语言,编译过程大多如上图。java中,javac编译器完成了词法分析、语法分析到抽象语法树,再遍历树生成线性指令流。这部分是在虚拟机外进行的,而解释器在虚拟机内部。编译器输出的指令流是一种基于栈的指令集架构,依赖操作数栈工作。主流pc指令集是依赖寄存器工作。基于栈的好处是不直接依赖硬件,可以执行好一些,但缺点是相比寄存器,会慢一些,而且会产生数量更多的指令。
解释器在运行时,会对载入的指令一条一条的执行,前面说过计算操作都是基于操作数栈进行的。
例:
2019120001593\_3.png

点赞(1)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入理解Java虚拟机----(七)字节码执行引擎

相关推荐