第八章 虚拟机字节码执行引擎

来源:互联网 发布:怎么处理淘宝的照片 编辑:程序博客网 时间:2024/05/22 13:42

1. 运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构
每一个栈帧中存储了方法的局部变量表,操作数栈, 动态连接和方法返回地址。每一个方法调用的开始直至执行完成的过程,都是一个对应的栈帧从入栈到出栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了栈帧具体分配多少的内存,取决于具体的虚拟机实现。而不会受到运行时期的数据变化的影响。
在活动的线程中,只有位于栈顶的栈帧才是有效的,这个栈帧称之为当前帧,。与这个栈帧关联的方法称之为当前方法。执行引擎运行的所有的字节码指令都只针对当前栈帧进行操作。
栈帧的结构图如下

                               栈帧的概念结构
1.1局部变量表
局部变量表示一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。Java程序在编译为Class文件时,就在方法的code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽位最小单位。对于一个32位的变量,可以使用一个变量槽就可以将其装入一个Slot中,比如基本变量类型:boolean, byte,short, int, float reference着这return Address。而对于64位的变量来说,会占用两个连续的slot空间, 由于64位的数据遵守非原子数据协定,所以会把long或者double类型的数据的读写分割为两次32位的读写操作,但是由于局部变量表在虚拟机站上,是线程私有的,所以无论读写两个连续的slot操作是否为原子操作,都不会有数据安全的问题,因为不存在并发的情形。
局部变量表中第0位索引的slot默认是用于传递方法所属对象实例的引用。

局部变量表slot复用会对垃圾收集造成一定的影响。局部变量表是GCRoots中的一种

补充:可作为GCRoots的对象包括下面结构中的对象:
1. 虚拟机栈中的引用的对象(存放于局部变量表中)。
2. 方法区中类静态属性引用的对象和常量引用的对象。
3. 本地方法栈中JNI引用的对象。

1.2操作数栈
操作数栈是用来进行存储方法在执行的过程中的一些指令的操作数和中间值的场所,在方法执行的过程中,会有各种的字节码指令往操作数栈中写入和读取内容,也就是出栈和入栈操作。同时这也是相对于基于寄存器的虚拟机的弱势,因为每次的出栈和入栈操作无疑多了很多的操作,造成数据的读取和写入比较繁琐,速度上是比基于寄存器 的虚拟机要慢的
java虚拟机的解释执行引擎为“基于栈的执行引擎", 其中的栈指的就是操作数栈。

1.3动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用就是为了支持方法调用过程中的动态连接。这些符号引用会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化成为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。这两个过程就是方法调用过程的两种方式。

关于运行时常量池:运行时常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载进来后进入方法区的运行时常量池中存放。运行时常量池除了保存从Class文件中描述的常量池中的内容,也会将从符号引用翻译出来的直接引用存储在运行时常量池中。并且运行时常量池具备动态性,可以在运行时将新的常量放入池中。

1.4方法返回地址
一个方法在开始执行之后,有两种方法可以退出方法的执行:
1. 执行引擎遇到了任意一个方法返回的字节码指令,根据是否有返回值会有不同的方法返回的字节码指令,这种退出的方法成为正常完成出口。
2. 方法在执行的时候遇到了异常,只要本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。这种退出方法称为异常完成出口。

1.5 附加信息
虚拟机规范允许具体的虚拟机实现增加一些虚拟机规范里面没有描述的信息到栈帧之中,这部分不重要。

2. 方法调用

Class文件的编译的过程中不包含传统编译中的连接步骤,一切方法的调用在Class文件里面存储的只是符号引用,而不是方法实际运行时在内存布局中的入口地址。这个特性给Java带来了强大的动态扩展能力。(符号引用和直接引用在类加载过程的笔记中讲解过)
2.1 解析
在类加载的解析过程阶段,会将其中一部分符号引用转化为直接引用,但是在这个阶段解析的只是方法在程序运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。这类方法主要包括:被private,static, final修饰的方法和实例构造函数。
解析调用一定是一个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用转换为可确定的直接引用。
注:虽然 final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,所以结果也是唯一的。
2.2 分派
分派调用可能是静态的也可能是动态的,根据分派的总量数可分为单分派和多分派。分派调用是Java中重载和重写的基础。
2.2.1 静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分配的动作实际上不是由虚拟机完成的
  1. Human human = new Man(); // Man的父类是Human
上面代码中的Human称为变量的静态类型(外观类型). Man称为变量的实际类型。
区分一下,2.1中的解析过程发生在类加载阶段,是由虚拟机完成的,而2.2.1中的静态分配是在编译期确定的,是由编译器来完成的。所以说这两个过程不冲突,而是不同层次上的不同的操作。比如静态方法会在类加载期间进行解析,而静态方法显然也是可以拥有重载版本的。重载在编译时期运作,解析在类加载时运作,根本不冲突嘛。
2.2.2 动态分派
invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把这种在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派。
invokevirtual指令在运行时的解析过程大致分为4步:
1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限验证,如果通过则返回此方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程
4. 如果始终没有找到合适的方法,则抛出异常。
2.2.3 单分派与多分派
Java中静态分派属于多分派类型, 动态分派属于单分派类型。 下面根据一个例子来进行对单分派和多分派的讲解。
单分派和多分派的区别主要是在于执行某种操作或者选择时,影响该操作或者选择的因素是一个还是多个,如果是一个的话,那就是单分派;如果是多个的话,那就是多分派。
  1. /**
  2. * 这个例子中前后只有一个方法,这个方法在类中进行了重载,在父子类中又进行了重写。
  3. * @author WM
  4. *
  5. */
  6. public class 笔记例子 {
  7. static class QQ {
  8. }
  9. static class _360 {
  10. }
  11. public static class Father {
  12. public void hardChoice(QQ arg) {
  13. System.out.println("father choose qq");
  14. }
  15. public void hardChoice(_360 arg) {
  16. System.out.println("father choose _360");
  17. }
  18. }
  19. public static class Son extends Father {
  20. public void hardChoice(QQ arg) {
  21. System.out.println("son choose qq");
  22. }
  23. public void hardChoice(_360 arg) {
  24. System.out.println("son choose _360");
  25. }
  26. }
  27. public static void main(String[] args) {
  28. Father father = new Father();
  29. Father son = new Son();
  30. father.hardChoice(new _360());
  31. son.hardChoice(new QQ());
  32. }
  33. }
下面分别从编译阶段和运行阶段来看看具体的执行过程
编译阶段:
在静态分配的过程中,选择重载方法的目标方法的依据有两点:一是静态类型是Father还是Son,而是方法参数是QQ还是360。这次选择结果的最终产物是产生了两条invokevirtual指令(因为hardChoice方法并没有private,static ,final修饰,也不是构造函数,所以需要使用invokevirtual指令进行调用),这两条指令的参数分别是运行时常量池中指向Father.hardChoice(360)和Father.hardChoice(QQ)方法的符号引用。因为在静态分派的过程中根据了两个宗量进行选择,所以静态分派属于多分派类型。
运行阶段:
在运行时,执行编译期生成的invokevirtual指令时唯一可以影响虚拟机选择的因素只有此方法接受者的实际类型。参数的类型不能影响虚拟机的选择。所以Java的动态分派是单分派。
2.2.4 虚拟机动态分派的实现
动态分派的实现不同的虚拟机有不同的差别。
动态分派在运行的过程中属于非常频繁的操作。最一般的方法就是在类的方法元数据中搜索合适的目标方法。每次都要搜索类的元数据性能低下, 所以出于性能的考虑,最常用的“稳定优化”的手段就是为每一个类在方法区中建立一个虚方法表(Virtual Method Table)(虚方法就是由invokevirtual指令调用的方法)。同样的在invokeinterface执行时也会用到接口方法表(Interface Method Table)。虚拟机通过使用虚方法表来代替元数据查找以提高性能。
下面通过上面的实际例子的虚方法表来看看虚方法表这种方法的实际运作过程。
 
                                                                               法表结构图
上图中Son重写了来自Father的全部方法,子类方法表中的地址将会替换成为指向子类实现版本的入口地址。但是Father和Son类都没有重写来自Object类中的方法,所以他们的方法表中所有从Object类中继承过来的方法都指向了Object类。
虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里的地址入口和父类相同方法的地址是一样的,都指向父类的实现入口。如果子类重写了这个方法,那么子类的方法表中的地址将会替换为子类实现版本的入口。方法表一般在类加载的连接阶段进行初始化。
注: 类加载的连接阶段指的是类加载过程中的验证->准备->解析过程的总和就是连接阶段。

 



 
 
0 0
原创粉丝点击