粗谈Java虚拟机4_执行引擎

栈帧

Java虚拟机栈 是程序执行所必须的数据结构,每个Java线程都有一个JVM栈, JVM栈中以栈帧为存储单元,栈帧中存储执行方法必要的局部表量表、操作数栈、动态链接、方法返回值等信息。每次调用方法都会创建一个新的栈帧,方法执行结束或退出,栈帧即消亡。JVM栈结构如下∶
在这里插入图片描述
以下面代码为例,来分析栈帧的结构:

public static int  add(){
	int a = 1;
	int b = 2;
	return a+b;
}

通过 javap 得出可视化的方法结构:
在这里插入图片描述

局部变量表(local)

 局部变量表中存储方法参数和方法体内局部变量,在编译为 class 阶段,就确定其大小,和操作数栈一并记录在Code属于中。
局部变量表以槽(Solt)为存储单位,double和long类型使用2个Slot大小,其余类型占用1个Slot。方法中声明了多少变量和多少个参数,局部变量表就多大(double,long +1),成员方法比静态方法多了 1Solt大小 用于指向当前类 this。

操作数栈(stack)

 操作数栈又叫做 求值栈 ,和寄存器性质一样,是执行指令必要的数据区,栈的深度也在编译期间就确定。还是以上面的程序为例,简要分析栈帧结构和字节码指令的执行:

  • 第一条 iconst_1指令 ,从名字就可得知为常量,后面跟的是字面量,i代表常量类型。指令为:将一个int类型的常量1 加载到操作数栈。
  • 第二条指令 istore_0,将操作数栈栈顶的 int 类型的值存储到局部变量表索引为 0 的位置上。
  • 第三、四条指令 和上面前两条指令一样,
  • 第五、六条的 load指令 分别将局部变量表,索引 0 和 1 的值压入栈。
  • 第七条 iadd 指令, 将栈顶两个 int 类型的值相加,并且将结果压入栈。
  • 第八条 ireturn 指令,返回栈顶 int 类型的值,方法执行结束。

 看到这儿,不禁产生疑问,计算一个如此简单的加法运算,竟然花费了整整 7 条指令(ireturn不算),前面 4 条指令都是,先把一个已知的值压入栈,再赋值给局部变量表,接着再从局部变量表压入栈,用作计算使用。难道不能在将 1 压入栈后,接着直接把 2 压入栈,再执行 add 命令,3条就搞定了?想了一下,前面的几条指令都是为了初始化局部变量表,假如后面还需要使用 a 变量,则需要从局部变量表中读取。虽然此处方法,后面已经没有代码再引用 a 变量,只能说 javac 还不够先进,或许给 javac 编译器定位的就是简单的解析翻译。

方法的参数,在方法调用时就已经加载到局部变量表了。因为方法调用传递的是实参,直接可以赋值。

动态链接

 每一个栈帧都包含对运行时常量池的引用。很好理解,无论是执行的方法,还是方法体内的引用类型,都需要通过符号引用去解析确定在内存中的偏移量(指针、位置)。

方法返回地址

 如果方法正常完成执行,根据返回指令来决定是否有返回值和返回值类型。方法正常完成执行,恢复调用者的状态,包括局部变量表、操作数栈,并增加调用者的 PC寄存器 跳过方法的调用指令,如果有返回值,则将返回值压入调用者的操作数栈。
如果在方法执行过程中抛出异常,并且该异常未捕获导致访问退出,永远不会给上层调用者产生任何返回值。

基于寄存器的执行引擎

 下面带大家学几个汇编指令,有利于加深对 Java 字节码指令的理解汇编环境搭建工具

在这里插入图片描述
 DOSBox0.74-win32-installer.exe一直next安装完成后,在安装目录下打开DOSBox 0.74 Options.bat文件,直接双击用记事本打开,我这边用NotePad++打开有问题。在文件最下面加上 MASM 的路径

mount c: E:/MASM

c:
在这里插入图片描述

E:/MASM更改为自己的磁盘路径即可。打开DOSBox.exe出现如下页面,8086 汇编环境即搭建成功。
在这里插入图片描述
接下来输入debug换行执行后输入r即可看到14个寄存器。
在这里插入图片描述
此时做一个 1+1=2 加法运算,接着上面的步骤输入执行a命令,输入mov ax,0001,mov bx 0001,add ax,bx三条汇编指令。 再按下 Enter件,输入t执行命令。三次t后 ax 寄存器值为 2。
在这里插入图片描述
mov称为传送指令,可以理解为赋值,三条汇编指令对应 Java 中的伪代码:

short ax = 1;

short bx = 1;

ax = (short) (ax+bx);

我们输入的三条汇编指令,都会先存到内存当中,CS+IP 寄存器指向下一条执行指令位的地址,也就是大家常说的PC寄存器
在这里插入图片描述
同时也可以使用 mov 指令,将寄存器中的值,回写到内存当中。接着上面的步骤输入执行a命令,输入汇编指令 mov [0],ax,即可给 ds 寄存器指向的 073F 段内存地址的第 0 个字节赋值为 ax 上的 2。
在这里插入图片描述
反之给寄存器赋值也是一样 mov ax,[0]。现在我们模拟的是x86的16位架构 8086CPU,x86处理器是二地址指令,一般形式为:

op dest,src

它要支持二元操作,就只能把其中一个源同时也作为目标。就像上面的 add ax,bx 指令,ax 寄存器做源又做计算后的存储目标地址。

arm处理器的主要指令集是三地址形式,一般形式为:

op dest, src1, src2

三地址指令正好可以指定两个源和一个目标,能非常灵活的支持二元操作与赋值的组合。

 再来看上面的 Java字节码指令 后面没有附带任何的源地址和目标地址 ( 零地址指令 ) ,执行完全依赖操作数栈。零地址形式的指令集一般是通过 基于栈的架构 来实现的,由于指令的源与目标都是隐含的,零地址指令的密度可以非常高——可以用更少空间放下更多条指令。因此在空间紧缺的环境中,零地址指令是种可取的设计。但零地址指令要完成一件事情,一般会比二地址或者三地址指令许多更多条指令。

基于栈与基于寄存器架构的VM,用哪个好?
实现简单

 基于栈架构的指令集生成代码的编译器更容易实现。

移植性

 在第一篇文章 粗谈Java虚拟机1_开山篇 就提到过硬件平台的差异性,arm 寄存器 本身就比 x86 架构的多。假如一个VM采用基于寄存器的架构(它接受的指令集大概就是二地址或者三地址形式的),为了高效执行,一般会希望能把源架构中的寄存器映射到实际机器上寄存器上。但是VM里有些很重要的辅助数据会经常被访问,例如一些VM会保存源指令序列的程序计数器(program counter,PC),为了效率,这些数据也得放在实际机器的寄存器里。如果源架构中寄存器的数量跟实际机器的一样,或者前者比后者更多,那源架构的寄存器就没办法都映射到实际机器的寄存器上;这样VM实现起来比较麻烦,与能够全部映射相比效率也会大打折扣。像Dalvik VM的解释器实现,就是把虚拟寄存器全部映射到栈帧(内存)里的,这跟把局部变量区与操作数栈都映射到内存里的JVM解释器实现相比实际区别不太大。

 如果一个VM采用基于栈的架构,则无论在怎样的实际机器上,都很好实现——它的源架构里没有任何通用寄存器,所以实现VM时可以比较自由的分配实际机器的寄存器。于是这样的VM可移植性就比较高。作为优化,基于栈的VM可以用编译方式实现,求值栈实际上也可以由编译器映射到寄存器上,减轻数据移动的开销。

 回到主题,基于栈与基于寄存器的架构,谁更快?看看现在的实际处理器,大多都是基于寄存器的架构,从侧面反映出它比基于栈的架构更优秀。
而对于VM来说,源架构的求值栈或者寄存器都可能是用实际机器的内存来模拟的,所以性能特性与实际硬件又有点不同。一般认为基于寄存器的架构对VM来说也是更快的,原因是:虽然零地址指令更紧凑,但完成操作需要更多的load/store指令,也意味着更多的指令分派(instruction dispatch)次数与内存访问次数;访问内存是执行速度的一个重要瓶颈,二地址或三地址指令虽然每条指令占的空间较多,但总体来说可以用更少的指令完成操作,指令分派与内存访问次数都较少。

摘自RednaxelaFX大大的 虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值