JVM-虚拟机字节码执行引擎

虚拟机字节码执行引擎

执行引擎是Java虚拟机核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机 器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层 面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执 行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。

  • 解释执行:通过解释器执行,每次都需要解释,时间较快
  • 编译执行:即时编译器产生本地代码执行,编译时间较长,但是只需要编译一次,之后无需编译

运行时栈桢结构

Java虚拟机以方法作为最基本的执行单元,栈桢是方法执行背后的数据结构,也是方法执行模型虚拟机栈中的元素

栈桢存储了方法的局部变量表,操作数栈,动态链接,方法返回地址和一些额外信息

  • 栈桢的大小编译时可知,多大的局部变量表,多深的操作数栈都在编译时写到方法表的code字段中的max_local

一个线程中的方法调用链可能会很长,以Java程序的角度来看,同一时刻、同一条线程里面,在 调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方 法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Current Stack Frame),与 这个栈帧所关联的方法被称为“当前方法”(Current Method)。执行引擎所运行的所有字节码指令都只 针对当前栈帧进行操作

在这里插入图片描述

局部变量表

局部变量表是一组变量值的存储空间,主要存放方法参数和方法内部定义的局部变量

Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变 量槽数量。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位 数据类型的变量,则说明会同时使用第N和N+1两个变量槽(一个槽存储32位的数据)

对于两个相邻的共同存放一个64位数据 的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个,《Java虚拟机规范》中明确要求 了如果遇到进行这种操作的字节码序列,虚拟机就应该在类加载的校验阶段中抛出异常。

对于两个相邻的共同存放一个64位数据 的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个,《Java虚拟机规范》中明确要求 了如果遇到进行这种操作的字节码序列,虚拟机就应该在类加载的校验阶段中抛出异常。

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

向内存填充了64MB的数据,然后通知虚拟机进行垃圾收集,但是发现JVM并没有回收这64mb

  • 因为在执行System.gc()时, 变量p还处于作用域之内,虚拟机自然不敢回收掉p的内存
    public static void main(String[] args) {
        {
            byte[] p = new byte[64 * 1024 * 1024];
        }
        System.gc();
    }

在这里插入图片描述

可以看到还是没有回收,p的作用域被限制在花括号以内,从代码逻辑上讲,在执行 System.gc()的时候,p已经不可能再被访问了

    public static void main(String[] args) {
        {
            byte[] p = new byte[64 * 1024 * 1024];
        }
        int a = 0; // 复用了p的变量槽
        System.gc();
    }

此时发现,p竟然被回收了

  • 根本原因就是:局部变量表中的变量槽是否还存有 关于p数组对象的引用。第一次修改中,代码虽然已经离开了p的作用域,但在此之 后,再没有发生过任何对局部变量表的读写操作,p原本所占用的变量槽还没有被其他变量 所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断, 绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面 又定义了占用了大量内存但实际上已经不会再使用的变量,手动将其设置为null值(用来代替那句int a=0,把变量对应的局部变量槽清空

操作数栈

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO) 栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项 之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占 的栈容量为1,64位数据类型所占的栈容量为2。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种 字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。譬如在做算术运算的时候是通过 将运算涉及的操作数栈压入栈顶后调用运算指令来进行的,又譬如在调用其他方法的时候是通过操作 数栈来进行方法参数的传递。举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要 求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int 值出栈并相加,然后将相加的结果重新入栈

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器 必须要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为 例,这个指令只能用于整型数的加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型, 不能出现一个long和一个float使用iadd命令相加的情况。

Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,里面的“栈”就是操作数栈。

动态链接

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

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

方法调用

方法调用阶段唯一的任务就是确定被调用方法的版本 (即调用哪一个方法)

class文件的编译过程不包含传统程序语言编译的连接步骤,一切方法调用在class文件里边存储的都只是符号引用,而不是方法在实际运行时内存布局的入口地址,需要进行静态或动态解析

解析

  • 静态解析,在类加载过程将一部分符号引用转为直接引用,方法在编译前已经确定好了一个可调用的版本 (静态方法,私有方法,final方法,静态方法:类型直接关联,静态方法,外部不可访问,父类或子类没法重写出其他版本)
  • 动态解析,运行期动态解析,将符号引用转为直接引用

分派

  • 静态类型,编译期可知的类型
  • 实际类型,运行时对象的实际类型

静态分派

  • 依赖静态类型来决定方法执行版本的分派是静态分派
    • 重载是依赖于参数的静态类型而不是实际类型去作为方法版本执行的判定依据
public class WaysToChange {

    static abstract class Human{
    }

    static class Woman extends Human{
    }

    static class Man extends Human{
    }

    public void sayHello(Human human){
        System.out.println("human");
    }

    public void sayHello(Man human){
        System.out.println("man");
    }

    public void sayHello(Woman human){
        System.out.println("woman");
    }

    public static void main(String[] args) {
        // 方法重载依赖于静态类型作判定,man和woman的静态类型都是Human,即输出都是执行void sayHello(Human human)的结果
        Human man = new Man();
        Human woman = new Woman();
        WaysToChange waysToChange = new WaysToChange();
        waysToChange.sayHello(man);
        waysToChange.sayHello(woman);
    }
}

在这里插入图片描述

需要注意Javac编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯 一”的,往往只能确定一个“相对更合适的”版本

动态分派

  • 判定执行方法的版本是依赖于实际类型
    • 重写的原理就是动态分派
public class Respace {


    static abstract class Human{
        protected abstract void say();
    }

    static class Man extends Human{
        @Override
        protected void say() {
            System.out.println("Man");
        }
    }

    static class Woman extends Human{
        @Override
        protected void say() {
            System.out.println("Woman");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        woman.say();
        man.say();
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_41922289/article/details/107596124