文章目录
目录
前言
主要介绍了Java虚拟机字节码执行引擎。
一、执行引擎
- Java虚拟机字节码执行引擎由软件自行实现,不受物理条件制约可以执行不被硬件直接支持的指令集格式;
- 输入字节码二进制流,输出执行结果,处理过程就是字节码解析执行过程可以选择解释执行、编译执行(会编译优化,实际执行过程与编写内容的执行过程不同但结果相同)或者两者兼备
二、栈帧
- 作用
- 一个方法从调用开始到执行结束对应一个栈帧在虚拟机栈中的入栈和出栈(虚拟机栈属于线程独占的)
- 在编译Java程序源码时,栈帧中局部变量表和操作数栈就计算出需要的最大内存;栈帧需要分配的内存,不会受到运行时变量数据的影响,只取决于程序源码和虚拟机栈内存布局形式
- Java程序认为堆栈中所有栈帧对应的方法都处于执行状态;执行引擎运行的字节码指令只针对当前位于栈顶位置的 ‘当前栈帧’ 关联的 ‘当前方法’
- 内容
- 局部变量表
- 用途
- 用于存放方法参数和方法内部定义的局部变量,在编译时就确定其最大容量
- 方法被调用时,局部变量表完成参数值到参数变量列表的传递(实参到形参的传递),实例方法(static静态方法可以用类名调用,实例方法必须对象调用)对应的局部变量表中第0位变量槽记录该方法所属实例对象的引用(可用this指针访问)
- 变量槽
- 局部变量表的最小单位,大小可以随着操作系统或者虚拟机不同而动态定义,但要求可以存放下基本数据类型,对64位系统的数据类型可以用两个相邻的变量槽存放一个数据,对于这两个变量槽不可以单独访问其中一个
- 变量槽的复用
- 目标:通过变量槽的复用,节省栈帧消耗的内存空间
- 过程:方法中定义的局部变量的作用域不一定是整个方法,若PC程序计数器超过某个变量的作用域,该变量的变量槽可以交给其他变量重用
- 注意:局部变量表作为GC Roots的一部分,与其相关联的变量槽中的变量在GC中无法被回收;若指令已经执行到某个变量的作用域之外,但是该变量槽还未被复用(即还存在于局部变量表中),此时该变量无法被正确回收;可以在变量使用完成后置null值防止这种情况出现,但是编译过程中该置null值操作很容易被编译优化而失效
- 类变量与局部变量区别
- 类变量会经过两个初始化过程(准备阶段系统初始化零值,初始化阶段按照开发人员意愿初始化),不赋初值也可以正常访问
- 方法中的局部变量在准备阶段不会被初始化零值,若不赋初值会在字节码校验(检查方法是否有问题)阶段出现类加载失败
- 用途
- 操作数栈
- 用途
- 用于存放方法中各种计算所需要的操作数,方法执行时字节码指令会往操作数栈中写入和提取数据(即入栈出栈),是一种后入先出栈
- 在编译时确定最大容量,在类校验阶段的字节码校验的数据流分析阶段,保证方法执行时操作数栈不会超过其最大深度,同时检查保证操作数栈中的元素数据类型与字节码指令序列严格匹配
- 在虚拟机中对栈帧进行优化
- 让两个栈帧部分重叠以节约空间,同时使得在方法调用时可共用一部分数据,避免额外的参数复制过程
- 用途
- 栈帧信息
- 动态连接
- 栈帧中含有一个指向运行时常量池中栈帧所属方法的引用(栈帧中的引用指向运行时常量池中的符号引用,常量池的符号引用指向方法)
- 常量池符号引用
- 静态解析
- 类加载、第一次使用时就解析为直接引用
- 动态连接
- 每次运行时都转化为直接引用
- 静态解析
- 方法返回地址
- 返回方式
- 正常调用完成
- 遇到方法返回的字节码指令,可能有返回值传递给调用者
- 异常调用完成
- 遇到异常,使用异常完成出口,不会有返回值
- 正常调用完成
- 方法退出后需返回到方法被调用的位置(正常完成时PC计数器记录,异常退出时异常处理器)
- 方法退出后一般需要执行
- 恢复作为调用者的上层方法的局部变量表和操作数栈
- 返回值压入调用者栈帧的操作数栈中
- 调整PC计数器值使其指向调用指令后一条指令
- 返回方式
- 附加信息
- 调试、性能收集
- 动态连接
- 局部变量表
三、方法调用
- 任务
- 方法调用唯一任务就是确定调用哪个版本的方法(与方法执行无关)
- 静态解析
- 静态解析条件
- 编译期可知,运行期不可变
- 静态解析的类
- 要求
- 无法通过继承重写出其他版本覆盖原版本,可以在编译期将符号引用静态解析成直接引用
- 五种非虚类
- invokestatic字节码指令
- 静态方法(与类型直接相关)
- invokespecial字节码指令
- 私有方法(在外部不可访问)
- 实例构造器<init>()方法
- 父类方法
- invokevirtual字节码指令(历史遗留问题
- final修饰的方法
- invokestatic字节码指令
- 要求
- 静态解析条件
- 动态连接
- 对象类型分类(A a=new B)
- 静态类型(a的静态类型是A)
- 在运行期间可以通过强制转换符号对对象的静态类型进行改变,但是对象本身静态对象不会改变,且在编译期可确定
- 实际类型(a的实际类型是B)
- 只有实际运行到new的指令时才可以确定a的实际类型,在编译期无法确认对象的实际类型
- 静态类型(a的静态类型是A)
- 分派
- 静态分派(在编译期间确定,也可以归于静态解析中)
- 定义
- 静态分派是方法实现重载的本质(方法重载=同名、同返回值、不同参数)
- 静态分派指在编译期间,方法重载时通过方法调用时参数的静态类型确定实际调用方法版本的解析过程
- 重载选择 ‘更适合的’ 版本
- 若方法调用时存在重载,但是方法参数的静态类型(char)与重载方法中参数类型(int long float)无法完全匹配,会将实际调用方法的参数进行静态类型转换(char--->int),选择与重载方法中类型最适合版本(int(>long>float))的方法进行调用
- 定义
- 动态分派
- 定义
- 动态分派是方法实现重写的本质,体现了java语言的多态性(继承+重写+父类引用(静态类型)指向子类对象(实际类型))
- 动态分派指在运行期间,方法重写时通过接受者(调用者)的实际类型确定确定执行版本的动态解析过程
- 字段不参与多态
- 子类调用方法时,父类中与子类同名的字段会被子类屏蔽
- 性能优化
- 虚方法表(接口方法表)
- 为每个类型维护一个方法表,子类未对父类方法进行重写,则父类与子类方法表中该方法字段同时指向父类类型数据;若子类对父类方法进行重写,则子类方法表中该方法字段会指向子类类型数据;通过记录方法表节约搜索接受者(调用者)类型的方法元数据过程对性能的消耗
- 虚方法表(接口方法表)
- 定义
- 宗量
- 静态多宗量
- 编译时需要确定接受者(调用者)的静态 类型 + 参数的静态类型
- 动态单宗量
- 编译期间已经确定参数的静态类型,运行时只需确定接受者(调用者)的实际类型
- 静态多宗量
- 静态分派(在编译期间确定,也可以归于静态解析中)
- 对象类型分类(A a=new B)