虚拟机字节码执行引擎

读深入理解JAVA虚拟机 第八章,记一下内容

虚拟机字节码执行引擎

概述

执行引擎在执行java代码的时候会有解释执行(通过解释器执行)和编译执行(通过即使编译器产生本地代码执行)两种选择,也可能两者兼备。

运行时栈帧结构

每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。
1,每个栈帧需要分配的内存大小,都是在编译代码的时候就已经确定并且写入到方法表的code属性中了。
2,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与整个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

局部变量表

一组变量值存储空间,存放方法参与和方法内部定义的局部变量
局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机没有规定一个Slot应占内存空间的大小,但是每个slot都应该能存放一个基本数据类型(int,float,byte,boolean,char,short),或者返回地址类型或者reference类型(对象实例的引用),这八种类型(不包括long,String),这八种类型都可以用32位或者更小的物理内存来存放,但是没有规定大小,这样虚拟机如果是64位的,也可以用64位去实现slot,然后用空白区域补齐多余的32位空间;

线程私有的;

slot的空间是在内存中真实分配的,这个空间是可以被局部变量重复使用的,如果有一个局部变量a,在代码块之中,现在在代码块之外,所以这时候如GC,理论上是能回收a所指向的内存的,但是如果slot没有进行任何操作,那么slot还是指向了这个内存的,所以GC的时候,会发现内存不能释放,因为还在被占用;

slot没有任何操作是指,在期间没有任何对局部变量表的读写操作,占用的slot还没有被其他变量复用;

所以如果碰见这种不在使用但是又占据大内存的操作,并且可能后面要进行一个长耗时的操作,并没有去复用这个slot,那么可以考虑手动把这个变量设置为null来释放内存。(不优雅,平时不用考虑)

代码只有在解释器执行时候才会有上述的回收情况,如果是JIT编译器编译,优化后的,这个赋null值的动作会被消除,而且代码也不会有这个gc问题,所以不需要用赋值的方式来清空旧代码;
字节码被编译为本地代码后,对GC Roots的枚举与解释执行期有巨大差别。

局部变量没有经过赋值(初始化)是不存在默认值的,所以是不能编译成功的

方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法
正常完成退出:(Normal Method Invocation Completion),执行引擎遇到任意一个方法方法的字节码指令,将返回值传递给上层的方法调用者(是否有返回值以及返回值类型将根据遇到的是哪种方法返回指令来决定)
异常完成出口:(Abrupt Method Invocation Completion)在方法执行过程中遇到了异常,并且没有在方法体内得到处理,一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

方法调用

方法调用阶段唯一的任务就是确定被调用方法的版本(调用哪一个方法),只是符合引用,不包括传统编译中的连接步骤(直接引用地址)

解析

在类加载的解析阶段,会将一部分的符合引用转化为直接引用,前提是,在方法程序真正运行之前,也就是方法调用阶段,就有一个可以确定的,并且在运行期不会改变的调用版本。
“编译器可知,运行期不可变”的主要有
静态方法,私有方法,实例构造器,父类方法4个类型,它们在类加载的时候就会把符号引用转化为直接引用。这些方法叫做非虚方法,final修饰的因为不能改变,所以也是一种非虚方法
其他方法叫做 虚方法(不含final的方法)。

分派

分派调用,(Dispatch)可能是静态的,也可能是动态的,分为单分派和多分派,以及静态vs动态,四种情况

静态分派

Human man = new Man();

这里的Human称为变量的静态类型/外观类型,后面的Man称为变量的实际类型

虚拟机(编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据的。静态类型是编译器可知的,而实际类型变化的结果要在运行期才可以确定。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。典型应用就是重载。
有时候,这种重载方法的版本不是“唯一”的,所以只是选了一个“更适合的”版本
比如说,调用的时候如下甚至更多情况,会先选char,没有char的时候,会选int,按照char->int->long->float->double的顺序进行自动类型转换,选择优先级高的方法版本;

再下一个优先级是Character,字段类型转换找不到的情况下,就进行自动装箱,然后是接口类型,比如Serializable;

可变长参数的优先级是最低的;同时在单参数中可以成立的一些自动类型转换,在可变长参数中是不成立的。

在现实中不应该出现这种极端代码。太绕
具体看P250页。

sayHello('a');
public static void sayHello(int arg){...}
public static void sayHello(char arg){...}

动态分派

静态类型和实际类型一样,(都是子类Man)子类和父类中都有同名方法,先找子类,子类没有就找父类的方法,这个就是动态了。
运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

宗量:子类和父类重载选择是一种,同方法类同名函数的重写也是一种,两种宗量
静态分派属于多分派类型
动态分配数单分派类型(具体看P256)

虚拟机的优化手段:为类在方法区中建立一个虚方法表(Vritual Method Table,vtable)以及对应的接口方法表(Interface Method Table, itable)。使用虚方法表索引来代替元数据查找以提高性能。
存放了各个方法的实际入口,如果子类重写了父类方法,子类方法表中的地址将会被替换为指向子类实现版本的入口地址,如果没有重写,就是指向父类的实现入口地址。

动态类型语言

动态类型语言:比如php,JavaScript,特点,“变量无类型,而变量值才有类型”,灵活

静态类型语言:比如java,c++,需要在编译器确定(变量)类型,严谨检查

invoke包,MethodHandle 动态确定目标方法的机制。

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

分为解释代码执行和编译(本地代码)执行
大致流程:
程序源码->词法分析->单词流->语法分析->抽象语法树

解释执行的情况: ->指令流(可选)->解释器->解释执行
编译执行的情况: ->优化器(可选)->中间代码(可选)->生成器->目标代码

执行过程的字节码略 P.273

实际情况中,虚拟机中解析器和即使编译器都会对输入的字节码进行优化,所以运行的时候,都会比概念模型来的性能更高。或者描述的差距很大

阅读更多

没有更多推荐了,返回首页