所有的 Java 虚拟机的执行引擎都是一致的: 输入的是字节码文件, 处理过程是字节码解析的等效过程, 输出的是执行结果.
运行时栈帧结构
用于支持虚拟机进行方法调用和方法执行的数据结构, 是虚拟机栈的栈元素. 每一个方法从调用开始到执行完成的过程, 都对应一个栈帧在虚拟机栈中的入栈出栈过程.
由于虚拟机栈是线程私有的, 所以每一个线程都有一个自己的虚拟机栈, 而每个虚拟机栈都是由许多栈帧组成. 每一个栈帧都包括
局部变量表
操作数栈
动态连接
方法返回地址
额外附加信息
处于栈顶的称为当前栈帧, 对于执行引擎, 在活动线程中只有当前栈帧是有效的, 与当前栈帧关联的方法称为当前方法.
局部变量表
用于存放方法参数和方法内定义的局部变量. 虚拟机通过索引定位的方式使用局部变量表, 局部变量表的容量以变量槽 (Variable Slot) 为最小单位. 局部变量不像类变量那样有 "准备阶段", 不会被赋予系统初始值, 所以在定义局部变量时一定要对其赋值.
操作数栈
又被称为操作栈, 当一个方法开始执行时, 操作栈是空的, 随着方法的执行, 各种字节码指令往操作数栈中写入和提取内容, 也就是出栈和入栈的操作. 如在进行算术运算时就是通过操作数栈来进行的.
动态连接
每个栈帧都包括一个指向运行时常量池中该栈帧所属方法的引用, 持有这个引用是为了支持方法调用过程中的动态连接. Class 文件的常量池中有大量的符号引用, 字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数. 这些符号引用一部分会在类加载阶段或第一次使用时就转换为直接引用, 这种转化称静态解析; 另外一部分在每一次运行期间转化为直接引用, 称为动态连接.
方法返回地址
方法开始执行后, 只有两种方法可退出该方法:
正常完成出口: 执行引擎遇到任意一个方法返回的字节码指令;
异常完成出口: 执行过程中遇到异常, 在本方法中没有搜索到匹配的异常处理器而导致的退出.
方法退出的过程实际上等同于将当前栈帧出栈, 退出时可能执行的操作有: 恢复上层方法的局部变量表和操作舒展, 如有返回值, 把返回值压入调用者栈帧的操作数栈中, 调用 PC 计数值以指向方法调用指令后面的一条指令.
方法调用
方法调用不同于方法执行, 方法调用只是确定要调用哪一个方法, 还不涉及方法内部的具体运行过程. 所有方法调用在 Class 文件中都是一个常量池的符号引用, 在类加载甚至是运行期间才能确定目标方法的直接引用. 在类加载的解析阶段, 会将一部分的符号引用转化成直接引用(静态解析), 这种解析能成立的前提:
方法在程序真正运行之前就有一个可确定的调用版本
且这个方法在运行期间不可改变
满足上述条件的方法主要有两大类
静态方法, 直接与类型关联
私有方法, 在外部不能被访问
这两种方法各自的特点决定了它们不能通过继承或者别的方式重写其他版本, 因此它们适合在类加载阶段进行解析.
Java 虚拟机提供了 5 条方法调用字节码指令
invokestatic: 调用静态方法
invokespecial: 调用实例构造器 < init > 方法, 私有方法和父类方法;
invokevirtual: 调用所有的虚方法
invokeinterface: 调用接口方法, 会在运行时再确定一个实现此接口的对象;
invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法, 然后再执行该方法.
只要能被 invokestatic,invokespecial 指令调用的方法, 都可以在解析阶段确定唯一的调用版本, 符合这个条件的有静态方法, 私有方法, 实例构造器父类方法, 它们可以在类加载时就把符号引用解析为该方法的直接引用. 这些方法称为非虚方法, 除开 final 方法外的其他方法都称为虚方法.
分派
分派调用可能是静态的也可能是动态的.
静态分派
静态分派: 所有以来静态类型来定位方法执行版本的分派动作称为静态分配. 静态分配和重载的关系密切.
什么是静态类型, 举个例子, 比如类 Human,Man 和 Woman, 其中 Man 和 Woman 继承了 Human.
- package exercise;
- public class StaticDispatch {
- static class Human {}
- static class Man extends Human{}
- static class Woman extends Human{}
- public void someMethod(Human human) {
- System.out.println("Human");
- }
- public void someMethod(Man man) {
- System.out.println("Man");
- }
- public void someMethod(Woman woman) {
- System.out.println("Woman");
- }
- public static void main(String[] args) {
- Human man = new Man();
- Human woman = new Woman();
- StaticDispatch s = new StaticDispatch();
- s.someMethod(man);
- s.someMethod(woman);
- }
- }
上述 main 方法中, 称 Human 为静态类型, 或者外观类型, 而 Man 或者 Woman 被称为实际类型.
静态类型的变化仅仅在使用时发生, 变量本身的静态类型不会被改变, 并且最终的静态类型在编译期时可知的; 实际类型变化的结果在运行期才可确定, 编译器在编译程序的时候并不知道一个对象的实际类型是什么.
- // 实际类型变化
- Human man = new Man();
- man = new Woman();
- // 静态类型变化
- s.someMethod((Man) man);
- s.someMethod((Woman) man);
编译器在重载时是通过参数的静态的静态类型而不是实际类型作为判断依据的. 因此上面的例子中会打印两个 "Human" 而不是一个打印 "Man" 一个打印 "Woman".
如果在 main 中改为
- s.someMethod((Man)man);
- s.someMethod((Woman) woman);
将会分别打印 "Man" 和 "Woman".
动态分派
动态分配: 在运行期间根据实际类型确定方法的执行版本的分配过程. 动态分配和多态中的重写 (Override) 有密切的关联.
举个例子
- package exercise;
- public class DynamicDispatch {
- static abstract class Human {
- protected abstract void someMethod();
- }
- static class Man extends Human {
- @Override
- protected void someMethod() {
- System.out.println("Man");
- }
- }
- static class Woman extends Human {
- @Override
- protected void someMethod() {
- System.out.println("Woman");
- }
- }
- public static void main(String[] args) {
- Human man = new Man();
- Human woman = new Woman();
- man.someMethod();
- woman.someMethod();
- man = new Woman();
- man.someMethod();
- }
- }
上面的例子会打印
Man Woman Woman
单分派和多分派
方法的接收者和方法参数统称为方法的宗量, 根据分派基于多少种宗量, 可以将分派分配划分为多分派和单分派.
如果在分派过程中既要依据方法接收者而要依据方法参数, 就是多分派, Java 的静态分派属于多分派; 如果在分派过程中只有某一种宗量作为选择依据, 其他宗量不会影响对虚拟机的选择, 比如方法参数不影响虚拟机选择, 唯一可以影响虚拟机选择的因素只有此方法的接收者, 则是单分派, Java 的动态分派属于单分派.
总结一下: 目前 Java 是一门静态多分派, 动态单分派的语言.
- by @sunhaiyu
- 2018.6.16
来源: http://www.bubuko.com/infodetail-2651787.html