虚拟机字节码执行引擎

虚拟机和物理机,二者都有代码执行能力,区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上,而虚拟机的执行引擎可以自己实现,因此可以自定义指令集和执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式

执行引擎是java虚拟机最为核心的组成部分,不同虚拟机实现里,执行引擎在执行java代码的时候可能有解释执行和编译执行或者两者兼备。但是从外观来看,所有的java虚拟机执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果


 运行时的栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

每一个方法从调用开始到执行完成的过程,就对应一个栈帧在虚拟机栈里从入栈到出栈的过程。

编译程序代码的时候,栈帧需要多大的局部变量表、多深的操作数栈都已经完全确定了,因此一个栈帧需要分配多少内存不会受到运行期变量数据的影响。

活动线程中只有栈顶的栈帧是有效的,称为当前栈帧,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。 

1.局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。最小单位是slot称为变量槽。

public static void main(String[] args){
  {
       byte[] placeholder = new byte[64*1024*1024];
   }  
   System.gc();
}

===========

public static void main(String[] args){
  {
       byte[] placeholder = new byte[64*1024*1024];
   }  
   int a = 0;
   System.gc();
}

局部变量表中的slot是可重用的,如果超过某个变量的作用域,那么这个变量对应的slot就可以交给其他变量使用。(不仅节省栈空间还会影响到垃圾回收行为)

第一种情况,代码虽然离开了placeholder的作用域,但是之后没有任何对局部变量表的读写操作,placeholder原本占用的slot还没有被其他变量复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。  而第二种情况就会复用原来的slot,此时会导致内存被回收。

 

局部变量不会有“准备阶段”,即不会赋予它初始值,一个局部变量如果定义了但是没有赋初始值是不能使用的。//   定义的时候不会报错,使用的时候会提示未初始化

2.操作数栈

iadd指令运行时,要求操作数栈中最接近栈顶的两个元素已经存入两个int数值,执行这个指令时,会将这两个int值出栈相加,然后将相加的结果入栈。

3.动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用的过程中的动态连接。

字节码中的方法调用指令就以常量池中指向方法的符号引用为参数,这些符号一部分会在类加载阶段或第一次使用的时候转化为直接引用,这些转化称为静态解析。另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接

4.方法返回地址

当一个方法被执行后有两种方式退出这个方法,第一种方式是执行引擎遇到任意一个方法返回的字节码指令。这种称为正常完成出口。

另一种退出方式是,在方法执行过程遇到了异常,并且这个异常没有在方法体内得到处理。这种退出方法称为异常完成出口。

无论采用哪种方法退出,方法退出后,都要返回到方法被调用的位置,程序才能继续,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。

方法退出的过程实际上等同于把栈帧出栈,可能执行的操作有:恢复上层方法的局部变量表和操作数栈把返回值压入调用者栈帧的操作数栈中调整PC计数器的值以指向方法调用指令后面的一条指令等。


方法调用

Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于直接引用)这个特性为java带来了更强大的动态扩展能力。

1.解析

在类的解析阶段,会将一部分符号引用转化为直接引用,这种解析成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可变的。(静态方法、私有方法、构造方法还有final修饰的方法,这几类方法无法被重写称之为非虚方法

2.分派

Human man = new Man()  //human称为变量的静态类型,man称为变量的实际类型,编译器在编译阶段并不知道对象的实际类型。

编译器在方法重载时,是通过参数的静态类型而不是实际类型来作为判定依据的。

所有依赖静态类型来定位方法执行版本的分派动作都称为静态分派。典型应用就是方法重载。静态分配发生在编译阶段

Java是一门静态多分派(重载,方法参数和方法接收者,编译阶段)、动态单分派(重写,方法的接收者,运行阶段)的语言。


基于栈的指令集与基于寄存器的指令集

java编译器输出的指令流,基本上是一种基于栈的指令集架构,这些指令依赖操作数栈进行工作,优点是可移植性,寄存器直接由硬件提供,程序如果直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。缺点是由于频繁的出栈入栈,导致执行速度相对较慢。

x86二地址指令集,基于寄存器的指令集,这些指令依赖寄存器进行工作

 

posted @ 2017-07-14 17:16  丨核桃牛奶  阅读(291)  评论(0编辑  收藏  举报