JVM字节码执行引擎

时间:2022-01-07 18:10:51

这里的概念太多,我自己的理解可能也不到位,因此为了保证大家理解的正确性,概念我基本都是复制的,没有像以往一样以自己的理解带入


什么是字节码执行引擎?用来做什么的?
执行引擎负责具体的代码调用及执行过程。就目前而言,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件、处理过程是等效字节码解析过程,输出的是执行结果。

运行时候的栈结构:每一个线程都有一个栈,栈中的基本元素我们称之为栈帧.栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构.
栈桢内容(主要三种):局部变量表、操作数栈、方法的返回地址
执行过程:栈帧中需要多大的局部变量表和多深的操作数栈在编译代码的过程中已经完全确定,并写入到方法表的Code属性中.在活动的线程中,位于当前栈顶的栈帧才是有效的,称之为当前帧,与这个栈帧相关联的方法称为当前方法.执行引擎运行的所有字节码指令只针对当前栈帧进行操作.
执行意义:每个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

局部变量表:
一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量.在编译期间该方法所需要分配的局部变量表的最大容量就已经确定.由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题,(这里的潜台词是,栈不用考虑线程安全问题)
包含类型:boolean、byte、char、short、int、float、reference或returnAddress类型八种类型

重点说一下reference:能从reference引用中直接或间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据

局部变量不像前面介绍的类变量那样存在”准备阶段”.我上篇文章中提到过,类变量在加载过程中要经过两次赋初始值的过程:一次在准备阶段,赋予系统初始值,另外一次在初始化阶段,赋予程序员定义的初始值.但局部变量不一样,如果一个局部变量定义了但是没有赋初始值是不能使用的.因此类变量可以不初始化就能使用,而局部变量必须初始化.

操作数栈
也叫操作栈,它是一个后入先出栈.和局部变量表一样,操作数栈的最大深度也在编译编译时就确定下来了.
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作.
方法返回地址
当一个方法被执行后,有两种方式可以退出这个方法。

  • 第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,这种退出方法的方式称为正常完成出口。
  • 在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种方式成为异常完成出口.此方式不会给上层调用者产生任何返回值。
    无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置:方法正常退出时,调用者的pc计数器的值作为返回地址;而通过异常退出的,返回地址是要通过异常处理器表来确定,栈帧中一般不会保存这部分信息.本质上,方法的退出就是当前栈帧出栈的过程。

最难的地方来了


方法调用
方法调用的主要任务就是确定被调用方法的版本(即调用哪一个方法),该过程不涉及方法具体的运行过程.按照调用方式共分为两类:

  • 解析调用时是静态的过程,在编译期间就完全确定目标方法。
  • 分派调用则即可能是静态,也可能是动态的,根据分派标准可以分为单分派和多分派。两两组合有形成了静态单分派、静态多分派、动态单分派、动态多分派

解析调用:在Class文件中,所有方法调用中的目标方法都是常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转为直接引用,也就是在编译阶段就能够确定唯一的目标方法,这类方法的调用成为解析调用.主要包含以下几种方法:静态方法、私有方法、实例构造器、父类方法
分派调用:
 静态分派:所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派,静态分派的最典型应用就是多态性中的方法重载.

class Human{  
}
class Man extends Human{
}
class Woman extends Human{
}

public class StaticPai{

public void say(Human hum){
System.out.println("I am human");
}
public void say(Man hum){
System.out.println("I am man");
}
public void say(Woman hum){
System.out.println("I am woman");
}

public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticPai sp = new StaticPai();
sp.say(man);
sp.say(woman);
//输出
//I am human
//I am human
}
}

我们把上面代码中的”Human”称为变量的静态类型,后面的”Man”称为变量的实际类型.静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的,而实际类型变化的结果在运行期才可确定.


ps.这段是我从书上贴上来的,其实我看不懂,主要有以下两点:
1、静态类型、实际类型到底是怎么区分的?
2、”静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变”这句话又是什么意思?

勉强理解的版本如下:
在调用 say()方法时,方法的调用者都为 sp 的前提下,使用哪个重载版本,完全取决于传入参数的数量和数据类型.代码中刻意定义了两个静态类型相同、实际类型不同的变量,可见编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的.并且静态类型是编译期可知的,所以在编译阶段,javac 编译器就根据参数的静态类型决定使用哪个重载版本。


动态分派:动态分派与多态性的另一个重要体现——方法重写有着很紧密的关系.举个简单例子Animal animal = new Cat();animal.run(),如果用静态类型来判断,那么无论怎么进行向上转型,都只会调用父类中的方法,但实际情况是,根据对父类实例化的子类的不同,调用的是不同子类中覆写的方法,很明显,这里是要根据变量的实际类型来分派方法的执行版本的.而实际类型的确定需要在程序运行时才能确定下来,这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派
单分派和多分派:方法的接收者和方法的参数统称为方法的宗量.根据分派基于宗量多少(接收者是一个宗量,参数是一个宗量),可以将分派分为单分派和多分派.单分派是指根据一个宗量就可以知道调用目标(即应该调用哪个方法),多分派需要根据多个宗量才能确定调用目标.
静态分派属于多分派类型;动态分派属于单分派类型


整理完了,但并没有完全理解,后面还会花些时间来看的.如果大家有更深的理解,欢迎一起探讨