• 虚拟机字节码执行引擎-----运行时栈帧结构


    执行引擎是java虚拟机最核心的组成部分之一。在java虚拟机中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成了各种虚拟机执行引擎的统一外观。在不同的虚拟机实现里面,执行引擎可能会有解释执行和编译执行两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎,但是从外观上看起来,所有的java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

    运行时栈帧结构

         栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。编译代码的时候,需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入了方法表的Code属性当中。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

      概念模型:

    1.局部变量表

    局部变量表是一组变量存储空间,用于存放方法参数和方法内定义的局部变量。以变量槽位最小单位(虚拟机规范中并没有明确指明一个变量槽到底多大,只是说每个变量槽都能存放一个boolean byte char short int float reference returnAddress类型的数据),这些都是32的长度的内存空间,所以变量槽会随情况而自动调整,对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的内存槽空间。64位的数据类型只有long和double两种,这里把long和double数据类型读写分割为两次32位读写的做法与“long和double的非原子性协定”中把一次long和double数据类型读写分割为两次32位读写的做法有些类似。

     虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表最大变量槽数量。如果是访问的是32位的数据,那么索引n就代表使用第n个变量槽,如果访问的是64位,那么就说明的是使用了n和n+1两个变量槽。对于两个相邻变量槽储存的64位数据,不允许使用任何方式单独访问其中的某一个。

    方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表地0位索引变量槽默认是用于传递方法所属对象的实例引用,其与参数则按照参数表顺序排列,占用从1开始的局部变量变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其与的变量槽。

    为了尽可能节省栈帧空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域不一定会覆盖整个方法体,如果当前字节码PC结束期的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量使用,不过这样的设计会带来一些额外的副作用。

    例如:变量的复用可能会直接影响系统的垃圾收集行为

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

    这时候没有回收,因为placeholder对象还处于作用域之内。

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

    加了花括号,placeholder对象的作用域被限制在了花括号之内,从代码逻辑上讲,在执行System.gc()的时候,placeholder对象已经不可能再被访问了,但是发现还是没有被回收,再改一下

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

    在调用System.gc()方法之前加一行int a=0,虽然很莫名其妙,但是对象被回收了。

    在这三段代码中,placeholder对象能否被回收的根本原因是:局部变量表中的变量槽中是否还存有关于placeholder数组对象的引用。第一次修改中,虽然代码已经离开了placeholder的作用域,但是之后,没有任何对局部变量表的读写操作,placeholder原本所占用的变量槽还没有被其他变量复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联,这种关联没被及时打断。可以选择将不使用的对象手动赋值为null,来代替int a=0;

    还有一点是局部变量没有类变量那样的“准备阶段”因此必须赋初值。

    2.操作数栈

    操作数栈可以放任何类型的java数据,32位数据类型所占的栈容量为1,64位为2,在任何时候,操作数栈的深度都不会超过code表中设置的最大值。

    操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段数据流分析中还要再次验证这一点。

    另外,在概念模型中,两个栈帧作为虚拟机栈的元素,是完全互相独立的。很多虚拟机里会做一点优化,让下面栈帧的部分操作数栈和上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据

    java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中的栈指的就是操作数栈。

    3.动态连接

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

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

    4.方法返回地址

    一个方法运行以后,有两种方式可以退出这个方法:

    ①执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法称为正常完成出口

    ②方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是虚拟机内部的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式叫异常完成出口

    无论哪种退出方式,方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,程序计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

    5.附加信息

    虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中。

    一般会把动态连接、方法返回地址与其他附加信息称为栈帧信息

  • 相关阅读:
    golang通知协程退出
    Linux Centos7下安装Elasticsearch
    es
    Laravel 别名 Facades的用法
    SVN使用时遇到的小问题
    http 重定向到 https
    Git-版本回退
    selenium---上传文件(非input标签)
    selenium---生成BeautifulReport报告
    windows搭建ngnix图片服务器
  • 原文地址:https://www.cnblogs.com/wxw7blog/p/7262107.html
Copyright © 2020-2023  润新知