深入理解Java虚拟机----第八章:虚拟机字节码执行引

目录

第一章:走进Java
第二章:Java内存区域与内存溢出异常
第三章:垃圾收集器与内存分配策略
第四章:虚拟机性能监控与故障处理
第五章:调优案例分析与实战
第六章:类文件结构
第七章:虚拟机类加载机制
第八章:虚拟机字节码执行引
第九章:类加载及其执行子系统的案例与实战
第十章:早期(编译器)优化
第十一章:晚期(运行期)优化
第十二章:Java内存模型与线程
第十三章:线程安全与锁优化

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

    • 目录
    • 第八章:虚拟机字节码执行引
      • 8.1概述
      • 8.2运行时栈帧结构
        • 8.2.1局部变量表
        • 8.2.2操作数栈(操作栈)
        • 8.2.3动态连接
        • 8.2.4方法返回地址
      • 8.3方法调用
        • 8.3.1解析
        • 8.3.2分派(重载、重写)
        • 8.3.3动态类型语言支持
    • 8.4基于栈的字节码解释执行引擎
        • 8.4.1解释执行
        • 8.4.2基于栈的指令集和基于寄存器的指令集
        • 8.4.3基于栈的解释器执行过程

8.1概述

执行引擎:输入字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果

8.2运行时栈帧结构

栈帧:是虚拟机用于方法调用方法执行的数据结构,是虚拟机运行时数据区的虚拟机栈的栈元素

栈帧存储了方法的局部变量表操作数栈动态连接方法返回地址等信息

每一个方法从调用开始到执行结束,就是栈帧在虚拟机栈中入栈出栈的过程

在编译期间,栈帧需要多大的局部变量表、多深的操作数栈都已经完全确定,并且写入方法的code 属性中

在活动线程中,栈顶的栈帧才是有效的,称为当前栈帧,相关联的方法称为当前方法

8.2.1局部变量表

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

在Java程序编译为class时,就在方法的code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量

局部变量表的容量以变量槽slot为最小单位

一个slot可以存放一个32位以内的数据,Java中32位以内的数据类型有:boolean、byte、char、short、int、float、reference、returnAddress

通过Reference类型可以:

  • 从此引用直接或者间接的获取到对象在堆上存放的起始地址索引
  • 此引用中直接或者间接的查找到对象所属数据类型在方法区的存储类型信息

对于64位(long,double)的数据类型,虚拟机采用高位对齐的方式为其分配两个slot空间

虚拟机通过索引定位的方式使用局部变量表

方法执行过程中,虚拟机通过局部变量表完成变量值到参数列表的传递过程

对于实例方法,局部变量表中第0位索引的slot默认传递方法所属对象实例的引用

为了节省栈帧空间,slot空间可以重用,但是会有额外的副作用,例如影响垃圾收集

8.2.2操作数栈(操作栈)

是一个后入先出栈

在编译时候写入code属性的max_stacks数据项中

操作数栈的每一个元素可以是任意的Java数据类型

32位数据类型占用的栈容量为1,64位数据类型占用的栈容量为2

操作数栈中元素的数据类型和字节码指令的序列严格匹配

概念模型上,两个栈帧完全相互独立,但大多虚拟机做了优化处理,使两个栈帧出现一部分重叠(方法调用可以共用一部分数据,无需进行额外的参数复制传递)

8.2.3动态连接

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

8.2.4方法返回地址

正常完成出口:PC计数器的值可以作为返回地址,栈帧中很可能保存这个计数值

异常完成出口:通过异常处理器表来确定返回地址,栈帧中一般不会保存这部分信息

方法退出等同于当前栈帧出栈,可能执行的操作有:

恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令

8.3方法调用

方法调用不等同于方法执行,唯一任务就是确定被调用方法的版本

8.3.1解析

调用目标在程序写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析

所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。

编译期可知,运行期不变的方法:静态方法、私有方法

相对应的5条方法调用字节码指令

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器init方法、私有方法、父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
  • invokedynamic:现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

只要能被invokestatic和invokespecial指令调用的方法,都可以再解析阶段中确定唯一的调用版本,符合这个条件的由静态方法、构造方法、私有方法、父类方法四大类,这些方法称为非虚方法(还包含final修饰的方法,无法被覆盖,没有其他版本);与之相反的称为虚方法(final修饰除外)

8.3.2分派(重载、重写)

静态分派(重载)

静态类型(外观类型):变量本身的静态类型不会被改变,最终的静态类型是在编译期可知的

实际类型:变化结果在运行期才确定

使用哪个版本的重载,完全取决于传入参数的数量和数据类型;虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据;并且静态类型是编译期可知的,因此,在编译阶段,javac编译器会根据参数的静态类型决定使用哪个版本的重载

所有依赖静态类型来定位方法执行版本的分派称为静态分派

静态分派的典型应用就是方法重载

动态分派(重写)

invokevirtual的运行时解析过程:

找到操作数栈顶的第一个元素指向的对象的实际类型,记作C

如果类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不存在,则返回java.lang.IllegalAccessError异常

否则,按照继承关系从下往上依次对C的各个父类进行第二步的查搜索和验证过程

如果始终没有找到,则抛出java.lang.AbstractMethodError异常

运行期根据实际类型确定方法版本的分派称为动态分派

单分派和多分派

方法的接受者方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派

单分派是根据一个宗量对目标方法进行选择

多分派是根据多于一个宗量对目标方法进行分派

静态分派属于多分派

动态分派属于单分派

虚拟机动态分派的实现

方法表

8.3.3动态类型语言支持

动态类型语言

关键特征是类型检查的主体过程是在运行期而不是编译期

变量无类型而变量值有类型

静态类型语言在编译期提供严谨的类型检查

动态类型语言提供了更大的灵活性

JDK7与动态类型语言

invokedynamic指令以及java.lang.invoke包出现

java.lang.invoke包

提供了一种动态确定目标方法的机制,称为MethodHandle

MethodHandle和反射(Reflection)区别:

本质上将都是在模拟方法调用,但反射模拟Java代码层次的调用,MethodHandle模拟字节码层次的调用

反射中的java.lang.Method对象远比MethodHandle机制的java.lang.MethodHandle对象所彪悍的信息多,反射是重量级,MethodHandle是轻量级

MethodHandle优化

invokedynamic指令

每一处含有invokedynamic指令的地方都称为动态调用点

掌控方法分派规则

8.4基于栈的字节码解释执行引擎

8.4.1解释执行

javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,在遍历语法树生成线性的字节码指令流的过程

一部分在虚拟机之外进行,而解释器是在虚拟机内部,所以Java程序的编译是半独立的实现

8.4.2基于栈的指令集和基于寄存器的指令集

基于栈的指令集主要优点是可移植性、代码更加紧凑、编译实现更简单;缺点是执行速度相对慢点

8.4.3基于栈的解释器执行过程

深入理解Java虚拟机----第八章:虚拟机字节码执行引_第1张图片

你可能感兴趣的:(java,JVM虚拟机)