《深入理解JVM虚拟机》 - 执行引擎
执行引擎是虚拟机的核心,物理机的执行引擎建立在处理器/硬件/指令集/OS层面;虚拟机的执行引擎自行制定指令集。虚拟机规范制定了执行引擎的概念模型,也就是各种虚拟机执行引擎的统一外观(facade):输入是字节码并执行。
一、运行时栈帧结构
栈帧存储方法的局部变量表,操作数栈,动态连接/方法返回地址/附加信息(统称栈帧信息)。栈顶栈帧又称为当前有效栈帧
1. 局部变量表
存放方法参数和局部变量值。最大容量在编译时确定并写入类文件 -- 方法的Code属性 -- max_locals。
局部变量表的最小单位是slot,每个slot能存放一个boolean/byte/char/reference类型的数据,两个slot存放一个double/long。reference类型用于查找对象在堆中的起始地址和查找类在方法区的存储信息。slot可重用,当代码执行离开局部变量的作用域时,该变量的slot可以分配给其他局部变量。实例方法局部变量表的第0个slot存储方法所属对象实例的引用,也就是this。
2. 操作数栈
最大深度在编译时确定并写入类文件-- 方法Code属性 -- max_stacks。方法开始执行时,操作数栈为空;执行过程中,字节码指令往操作数栈写入/提取内容。如iadd指令将栈顶两个int值出栈并相加,然后结果入栈。
概念模型中栈帧相互独立,但实现中会优化:下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,从而方法调用时可共用部分数据,省去额外的参数复制传递。
3. 栈帧元数据
1)动态连接:字节码中的方法调用指令参数为类文件-- 常量池-- 指向方法的符号引用,符号引用的解析分为静态解析/动态解析。静态解析:符号引用在类加载或者第一次使用时转化为直接引用;动态解析-动态连接:运行时转化为直接引用
2)方法返回地址:方法退出需要返回到调用方法的位置,操作如下:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者堆栈的操作数栈,调整PC计数器的值指向方法调用指令后面一条指令。
3)附加信息:虚拟机规范允许实现增加规范中没有的额外信息
二、方法调用
方法调用就是确定被调用方法的版本的过程,也就把类文件中的方法符号引用转化为直接引用的过程。字节码指令:invokestatic/special/virtual/interface/dynamic等。
1. 解析调用
编译时确定的静态过程,在类加载的解析阶段就把符号引用转化为直接引用。包括静态方法/私有方法/实例构造函数/父类方法;对应指令invokestatic/special。
2. 分派调用(dispatch)
1)静态分派:依赖静态类型来确定方法执行版本,典型应用是重载:Human a=new Man(); Human是变量a的静态类型,Man是变量a的实际类型。sayHello(a);调用的是sayHello(Human human)而不是sayHello(Man man);发生在编译阶段。在没有明确静态类型时存在模糊匹配,如‘a’
2)动态分派:依赖动态类型判断,典型应用时重写override,内部实现是invokevirtual找到对象的实际类型,并在类型中定位到调用方法。虽然final方法由invokevirtual调用,但final方法无法被覆盖,方法调用者无法进行多态选择。
虚拟机动态分配的实现:在类的方法区建立虚方法表,存放方法的实际入口地址,如果子类方法没有重写父类方法,则子类的虚方法表里的地址和父类相同;如果重写了,则替换为子类实现版本的入口;相同签名的方法在父类/子类的虚方法表中索引序号相同,方便类型变换时进行检索。方法表的初始化在类加载的初始化阶段完成。
3)单分派与多分派:方法的接受者(所属对象)和方法的参数统称为方法的宗量。根据分派基于多少种宗量划分单分派与多分派。
3. 动态类型语言支持
Invokestatic/special/virtual/interface指令,第一个参数都是被调用方法的符号引用,而动态语言运行时才确定接受者类型,因此不满足需求。jdk7在指令层面引入invokedynamic,在代码层面引入methodHandle(类方法指针)来支持动态类型语言。
4. 基于栈的字节码解释执行引擎
1)基于栈的指令集:Javac编译结果是基于栈的指令集架构,依赖操作数栈进行工作。计算1+1:入栈两个1,执行指令,出栈相加,结果入栈。优点是可移植:寄存器由硬件直接提供,程序直接依赖寄存器则受到硬件约束。虚拟机没有这个限制,可以自行决定把访问频繁的数据(程序计数器)放在寄存器以获取好的性能。缺点是执行速度相对慢,栈在内存中,频繁的栈访问意味着频繁的内存访问,优化手段如栈顶缓存等。
2)基于寄存器的指令集:x86指令集,依赖寄存器进行工作。计算1+1: 寄存器值设为1,add指令+1,结果保存在寄存器。
三、重载与重写
1)重载:boolean methodName(int input); boolean methodName(String input);
2)重写override,在子类中重写父类的方法
3)多态:虚方法,在调用方法之前不知道调用的是哪个对象的方法。接口A提供方法a,有三个实现类A1、A2、A3,都实现/重写了a方法。