深入学习JVM - (7) 字节码执行引擎

  • 7.1 执行引擎

    • 虚拟机的执行引擎是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式;
    • 《规范》制定了JVM执行引擎的概念模型,即JVM的统一外观,不同厂商实现时可能选择解释执行或编译执行或者二者兼备;
    • 从外观上看,所有JVM的执行引擎输入和输出都是一致的:输入字节码二进制流,处理过程是字节码解析执行的等小过程,输出执行结果;
  • 7.2 运行时栈帧结构

    • 结构图示

    • 是虚拟机运行时数据区的虚拟机栈的栈元素,对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame);
    • (1)局部变量表

      • 是存放方法参数和方法内部定义局部变量的存储空间;
      • 方法需要分配的局部变量表最大容量在编译时已经由方法的Code属性的max_locals数据项中确定;
      • 局部变量表的容量一变量槽Slot为最小单位,一个slot占用的内存大小没有明确规定,只是说到每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这使得变量槽的长度可以随着处理器、操作系统或虚拟机实现的不同而发生变化;
      • Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量;32位数据类型->索引N到表第N个变量槽,64位->同时使用N和N+1两个变量槽(不允许单独访问其中一个,否则字节码校验时抛出异常);
      • 当方法被调用时,JVM使用局部变量表完成实参到形参的传递;执行实例方法时索引为0的槽默认存放调用方法实例的引用this;
      • 局部变量表中的变量槽可以重用
    • (2)操作数栈

      • 操作数栈的最大深度在编译的时候被写入到Code属性的max_stacks数据项之中;
      • 操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2;
      • 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作;
      • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器必须要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点;
      • 另外在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递了;
    • (3)动态连接

      • 静态解析:“将常量池中指向方法的符号引用转化为直接引用”在类加载阶段或第一次使用时进行则为静态解析;
      • 动态连接:这一动作在运行期转化则为动态连接;
    • (4)方法返回地址

      • 方法有两种退出方式,正常调用完成和异常调用完成:方法正常退出时,主调方法的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值;方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息;
    • (5)附加信息

      • 《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现;
      • 在讨论概念时,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
  • 7.3 方法调用

    • 方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程,不等同于方法中的代码被执行;
    • 解析调用

      • 调用一个已经在运行之前就可以确定调用版本的方法,称为解析调用;(编译期确定,类加载期解析)
      • 只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。
    • 分派调用

      • (1)静态分派
        • 所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载。
        • ⚠️【虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的;】
        • 需要注意Javac编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一”的,往往只能确定一个“相对更合适的”版本;《== 产生这种模糊结论的主要原因是字面量天生的模糊性;
      • (2)动态分派
        • 与方法重写密切关联;
        • 这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
        • invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质;
        • 然而,字段永远不参与多态,哪个类的方法访问某个名字的字段时,该名字指的就是这个类能看到的那个字段;
      • (3)单分派与多分派
        • 方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择;
        • 如今的Java语言是一门静态多分派、动态单分派的语言;
      • (4)虚拟机动态分派的实现
        • 为每个类型创建一个虚方法表避免频繁在运行时到接收者类型的方法元数据中搜索合适的目标方法;
        • 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址;
        • 为了程序实现方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的虚方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址;
        • 虚方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也一同初始化完毕;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值