作用:输入字节码文件,解析字节码,并且输出结果。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧中存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用到执行完成的过程,都对应一个栈帧在虚拟机栈中从入栈和出栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经全部确定,并且写入了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行时变量数据的影响,而仅仅取决于具体的虚拟机实现。
在活动线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。
局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。程序编译完成时,就在方法的的Code属性的max_locals数据项中定义了该方法的所需分配的局部变量表的最大容量。
(1)局部变量表的容量以变量槽(slot)为最小单位,一个slot的内存占用并不确定,但每个slot应该能存放下boolean、byte、char、short、int、float、reference或者returnAddress类型的数据,这些可以使用32位或者更小的物理内存来存放,但允许slot的长度随着处理器、操作系统或者虚拟机的不同而发生变化。
(2)虚拟机对reference类型的要求:一是从此引用中直接或者间接地查找到对象在Java堆中的数据存放的起始索引地址;二是此引用中直接或者间接地查找到对象所属数据类型在方法区中的存储的类型信息。
(3)对于64位数据类型(long、double),虚拟机以高位对齐的方式为其分配两个连续的slot空间。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始到局部变量表的最大slot数量。如果访问的是32位数据类型的变量,索引n就代表了使用第n个slot,如果访问的是64位数据类型的变量,则说明会同时使用n和n+1两个slot。
在方法执行时,如果执行的是实例方法,局部变量表的第0位索引的slot默认是用于传递方法所属对象实例的引用,即”this”。其余参数按照参数表顺序排列,占用从1开始的局部变量slot,方法参数表分配完毕后,再根据方法内部定义的局部变量分配其余slot。
局部变量表作为 GC Roots的一部分,如果局部变量表中对象的引用一直存在或者没有被替换,该对象就不会被GC 回收。
一个先入后出的栈,其最大深度在编译的时候写入到了方法Code属性的数据项max_stacks中。32位的数据类型所占栈容量为1,64位数据类型的栈容量为2。在方法执行的时候,操作数栈的深度不会超过在max_stacks中设定的最大值。
操作数栈中元素的数据类型必须与字节码指令要求的类型严格匹配。
如果两个栈帧之间的操作数栈局部变量表存在共用数据,那么允许这两个局部变量表重叠,无需进行额外的参数复制传递。
字节码的方法调用指令以常量池中的指向方法的符号引用作为参数,如果符号引用在每一次运行期间转为符号引用,就称为动态链接。
当一个方法开始执行时,有两种退出方法的方式:①第一种:执行引擎遇到任意一个方法返回的字节码指令(如ireturn),这种称之为正常完成退出。
第二种方式是在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,无论是虚拟机内部产生的异常,还是代码使用athrow字节码指令产生的异常,只要是在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。这种称之为异常完成出口。
在方法退出之后,需要返回到方法被调用的位置,即调用点,继续执行其余程序。方法返回时可能需要在栈帧中保存一些信息,用于帮助其恢复它的上层方法的执行状态。方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时要通过异常处理器表确定,栈帧中不会保存这部分信息。
注意:方法退出等于把当前栈帧出栈,退出时操作有:恢复上层调用方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令的后面一条指令。
栈帧信息:一般指的是动态链接、方法返回地址与其他附加信息。
方法调用阶段的任务是确定被调用方法的版本(即调用哪一个方法),不涉及方法内部的具体运行过程。
1.所有方法调用中的目标方法在Class文件里面都是一个常量池的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的条件是方法在程序真正运行之前就有一个可确定的版本,并且这个方法的调用版本在运行期是不可变的。即调用目标在程序代码写好、编译器进行编译时就必须确定下来。这种调用称为解析。
2.符合”编译器可知,运行期不变“的方法,主要是静态方法和私有方法。静态方法与类型直接相关,私有方法在外部不可能被访问,因此这两种方法不可能通过继承或别的方法重写其他版本,因此适合在类加载阶段进行解析。
3.方法调用的字节码指令有invokestatic invokespecial invokevirtual invokeinterface invokedynamic
。只要能被invokestatic invokespecial
指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合条件的有静态方法、私有方法、实力构造器、父类方法4类。它们在类加载的时候就会把符号引用解析为该方法的直接引用。 这些方法称为非虚方法。final
修饰的方法也是非虚方法。
4.解析与分派的比较
解析是个静态的过程,编译期间就完全确定,在类加载的解析阶段会把涉及的符号引用全部转为直接引用,不会延迟到运行期再去完成。
分派可能是静态的,也可能是动态的。根据分派依据的宗量数可分为单分派与多分派,所以分派方式一共有4种:静态单分派、静态多分派、动态单分派、动态多分派。
分派显示了多态性特征。如重载、重写。
//代码1
public class StaticDispatch {
static abstract class Human{}
static class Man extends Human{}
static class Woman extends Human{}
public void sayHello(Human guy){
System.out.println("hello,guy!");
}
public void sayHello(Man guy){
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy){
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
Human man = new Man(); //Human为变量静态类型,Man为变量实际类型。
静态类型与实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用期发生(强转),变量本身的静态类型不变,并且最终的静态类型是在编译器可知的;而实际类型变化的结果在运行期才确定,编译器在编译时并不知道一个对象的实际类型是什么。
//实际类型变化
Human man = new Man();
man = new Woman(); //运行期确定
//静态类型变化
sr.sayHello((Man)man); //使用时
sr.sayHello((Woman)man);
代码 1中方法接受者已经确定是sr,虚拟机(准确说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据,确定方法版本的。并且静态类型是编译器可知的。在编译阶段,Javac编译器会根据参数的静态类型决定使用那个重载版本,所以选择了sayHello(Human)
作为调用目标,并把这个方法的符号引用写到main()
方法里的两条invokevirtual
指令参数中。
这种依赖静态类型来定位方法执行版本的分派动作成为静态分派。典型应用是方法重载。其发生在编译阶段,确定静态分派动作实际上由编译器完成。
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello(); //多态,方法重写
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
这里存在一个字节码指令invokevirtual
多态查找的过程。invokevirtual
的运行时解析过程分为:
这种在运行期根据实际类型确定方法执行版本的分派动作称为动态分派。
方法的接受者(持有者)与方法的参数统称为方法的宗量。根据分派基于多少种宗量,分派分为单分派(一个宗量)与多分派(多于一个宗量)。
public class Dispatch {
static class QQ {}
static class _360 {}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
@Override
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
@Override
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
(1)静态分派
选择目标方法的依据:一是静态类型是Father还是Son,二是方法参数是QQ还是360。由于根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。
(2)动态分派
运行期影响虚拟机选择目标方法的因素是hardChoice()方法的接受者的实际类型是Father还是Son。由于只有一个宗量选择,所以Java语言的动态分派属于单分派类型。
Java语言中,Javac编译器(虚拟机外部)完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。而解释器在虚拟机内部,所以Java的编译是半独立的编译。
优点:①可移植,寄存器由硬件直接提供。②代码相对更加紧凑、编译器实现更加简单。
缺点:执行速度相对慢。内存访问更加频繁。
实例参见《深入理解Java虚拟机》P272。