美文网首页
2020-10-10---第八章---虚拟机字节码执行引擎

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

作者: 李霖神谷 | 来源:发表于2020-10-10 14:13 被阅读0次

    1.运行时栈帧结构
    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)[插图]的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

    (1) 局部变量表:
    局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有“导向性”地说明每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。
    局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其他变量使用。这样的设计不仅仅是为了节省栈空间,在某些情况下Slot的复用会直接影响到系统的垃圾收集行为。
    示例:

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

    通过设置虚拟机参数-verbose:gc看运行结果:


    image.png

    这是因为当执行system.gc的时候a参数还在当前作用域,此时无法进行垃圾回收。

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

    此时可以看到,垃圾会被回收。因为slot被b复用并且清空。
    (2)操作数栈:
    操作数栈也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
    (3)动态链接
    每个栈帧都包含一个指向运行时常量池[插图]中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
    (4)方法返回地址
    当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者
    另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。
    一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
    2.方法调用:
    方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
    (1).解析:
    在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
    在Java语言中,符合“编译期可知,运行期不可变”这个要求的方法主要有静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。与之相对应,在Java虚拟机里面提供了四条方法调用字节码指令[插图],分别是:□ invokestatic:调用静态方法。□ invokespecial:调用实例构造器<init>方法、私有方法和父类方法。□ invokevirtual:调用所有的虚方法。□ invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器和父类方法四类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法就称为虚方法。

    (2)分派:
    解析调用是一个静态的过程,在编译期间就完全确定,不会延迟到运行期再去完成。
    分派调用则可能是静态的也可能是动态的。
    根据分派宗数量可分为单分派和多分派。这两类分派又可两两组合成:静态单分派,静态多分派,动态单分派和动态多分派4中分派组合。
    分派体现了Java的多态性,如“重载”和“重写”。
    静态分派:
    所有依赖静态类型(类型的引用)来定位方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
    动态分派:
    我们把在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

    相关文章

      网友评论

          本文标题:2020-10-10---第八章---虚拟机字节码执行引擎

          本文链接:https://www.haomeiwen.com/subject/wesdpktx.html