虚拟机在执行代码时如何找到正确的方法:解析、分派、动态类型支持
如何执行方法内的字节码:解释执行和编译执行
执行代码时涉及的内存结构:栈帧
一、执行引擎
输入字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
二、栈帧
是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素
当前栈帧:
- 局部变量表
- 操作数栈
- 动态连接
- 返回地址
局部变量表
- 变量值存储空间,存放方法参数和方法内部定义的局部变量
- slot 大小灵活
- 虚拟机通过索引定位的方式使局部变量表
操作数栈
- 先入先出栈
- 在方法执行过程中,字节码指令往栈中写入和提取内容
- 栈中元素类型必须和字节码指令的序列严格匹配
- 有时候栈帧重叠,用来共享数据
动态连接
- 常量池中的符号引用在每一次运行期间转化为直接引用,称为动态连接
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用
方法返回地址
两种结束方法的方式:
- 执行引擎遇到任意一个方法返回的字节码指令,PC 计数器会作为返回地址,回到方法调用处
- 在方法执行中产生了异常,通过异常处理器表来确定返回地址
三、方法调用
方法调用阶段的唯一任务就是确定该调用哪个方法,不涉及方法内部具体运行过程。
5 条方法调用字节码指令:
invokestatic
:调用静态方法invokespecial
:调用实例构造器方法,私有方法和父类方法 invokevirtual
:调用所有的虚方法invokeinterface
:调用接口方法,会在运行时确定一个实现此接口的对象invokedynamic
:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法
1、解析
- 解析是一个静态的过程,调用目标在编译时就应该确定。
- 在类加载的解析阶段,常量池中一部分的符号引用转换成直接引用。
- 只要能被
invokeststic 和 invokespecial
指令调用的方法,都可以在解析阶段确定唯一的调用版本
2、分派
1)静态分派
在编译期依赖静态类型来定位方法执行版本的分派动作称为静态分派。
虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的。
字面量没有显示的静态类型,所以只会选择最合适的进行重载。
2)动态分派
在运行期根据际类型确定方法执行版本的分派过程称为动态分派。
关键在于invokevirtual
指令的多态查找过程。
invokevirtual
指令的第一部就是在运行期间确定接收者的实际类型,所以会把常量池中类方法符号引用解析到不同的直接引用上。
3)单分派与多分派
单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。
java 的静态分派属于多分派
java 的动态分派属于单分派
4)虚拟机动态分派的实现
在方法区中建立虚方法表来避免频繁的搜索影响性能。
虚方法表中存放各个方法的实际入口地址。
方法表在类加载的连接阶段进行初始化,在准备了类的变量初始值之后,虚拟机会将该类的方法表也初始化。
3、动态类型语言支持
1)在运行期进行类型检查的语言是动态类型语言。
2)JDK1.7提供了 invoke 包和 MethodHandle 模仿虚拟机字节码指令调用行为,可以动态的确定目标方法
3)invokedynamic 指令也是为了将查找目标方法决定权,从虚拟机转到用户手中。
根据 invokeDynamic 常量中提供的信息,虚拟机可以找到并执行引导方法,从而获得一个 Callsite 对象,最终调用要执行的目标方法。
四、基于栈的字节码解释执行引擎
1、
解释执行:将编译好的字节码一行一行地翻译为机器码执行。速度更快
编译执行:以方法为单位,将字节码一次性翻译为机器码后执行。效率更高
程序源码—词法分析—单词流--语法分析-- 抽象语法树
— 指令流— 解释器 -- 解释执行
-- 优化器-- 中间代码— 生成器-- 目标代码
java 程序的编译是半独立的实现:
javac 编译器完成了从源码到字节码指令流的过程,这一部分工作是在 java 虚拟机之外进行的。解释器在虚拟机内部。
Java采用的是解释和编译混合的模式。它首先通过javac将源码编译成字节码文件class。然后在运行的时候通过解释器或者JIT将字节码转换成最终的机器码。
单独使用解释器的缺点:
抛弃了JIT可能带来的性能优势。如果代码没有被JIT编译的话,再次运行时需要重复解析。
单独使用JIT的缺点:
需要将全部的代码编译成本地机器码。要花更多的时间,JVM启动会变慢非常多;
增加可执行代码的长度(字节码比JIT编译后的机器码小很多),这将导致页面调度,从而降低程序的速度。
有些JIT编译器的优化方式,比如分支预测,如果不进行profiling,往往并不能进行有效优化。
因此,HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。
JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。
注:JIT为方法级,它会缓存编译过的字节码在CodeCache中,而不需要被重复解释。
2、基于栈的指令集可移植,执行速度慢一些。基于寄存器的指令集受到硬件约束,速度更快