虚拟机字节码执行引擎的概念模型,在执行java代码的时候可能会有解释执行和编译执行两种选择。所有的执行引擎都是:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
运行时栈帧结构
栈帧是支持虚拟机进行方法调用和执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。
在编译时,一个栈帧需要分配多少内存就完全确定了。只有当前栈帧是有效的。
- 局部变量表
存放方法参数和局部变量。以变量槽(Slot)存放数据,变量槽有索引,从0开始。0是当前对象的引用,用this关键字可以获取。其余是其他参数和局部变量,虚拟机通过索引取值。局部变量不像类变量,不会赋初始值,没赋值过就不能使用。 - 操作数栈
方法刚进入时操作数栈是空的,方法执行过程中会有指令不停进出栈。 - 动态连接
每个栈帧包含一个指向运行时常量池中该栈帧所属方法的引用,为了支持方法调用过程中的动态连接。 - 方法返回地址
在方法退出之后,需要返回到方法被调用的位置,保存这个信息。 - 附加信息
如调试相关信息。
方法调用
调用并不等于方法执行,调用阶段唯一的任务是:确定被调用方法的版本,不涉及具体运行过程。
- 解析
在类加载的解析阶段,会将一部分符号引用转化为直接引用,这种解析成立的前提是:方法在运行前就有一个可确定的调用版本,并且此版本在运行期不可改变。这类方法的调用成为解析。
符合“编译器可知,运行期不可变”要求的,主要包括:静态方法和私有方法。 分派
- 静态分派
所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。典型应用是方法重载。静态分派发生在编译阶段,因此不是由虚拟机来执行的。
这个例子的输出结果是:
hello, guy!
hello, guy!
我们把Human成为变量的静态类型,Man成为变量的实际类型。重载时时通过参数的静态类型而不是实际类型作为判定依据的。 - 动态分派
在运行期根据实际类型确定方法执行版本的分派过程成为动态分派。典型应用时方法重写。
这个例子的输出结果是:
hello man!
hello women!
hello women!
invokevirtual指令的过程大概是,找到找到操作数栈顶第一个元素的实际类型C。在C中查找与常量中完全相符的方法,如果找不到就层层往上,查找父类。这个过程第一步就是找实际类型,然后直接找方法执行。 - 单分派与多分派
方法的接收者与方法的参数统称为宗量,根据分派基于多少种宗量,将分派分为单分派和多分派。
java语言是一门静态多分派、动态单分派的语言。 - 虚拟机动态分派的实现
- 静态分派
动态类型语言支持
什么是动态类型语言?关键特征就是:类型检查的主题过程是在运行期而不是编译器,常用的包括:Groovy、JavaScript、Ruby等。
java.lang.invoke包目的是,提供一种新的动态确定目标方法的机制,称为MethodHandle。可以实现对方法的调用。
MethodHandle和反射的区别:- 反射是模拟java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用
- 反射中java.lang.reflect.Method对象远比MethodHandle机制中java.lang.invoke.MethodHandle对象包含的信息多。反射是重量级,MethodHandle是轻量级
- 反射是为了Java服务的,而MehtodHandler是为了虚拟机服务
基于栈的字节码解释执行引擎
执行引擎在执行java代码时都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。比如1+1的计算
基于栈的指令集:两个1入栈,然后出栈、相加,然后把结果放回栈顶。
基于寄存器的指令集:把EAX寄存器的值设为1,把这个值加1,结果保存在EAX寄存器中。