运行时栈帧结构
java虚拟机以方法作为最基本的执行单元。栈帧则是支持虚拟机进行方法调用和方法执行背后的数据结构,也是运行时数据区,虚拟机栈的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态链接 方法返回地址 以及PC计数器等信息
局部变量表
局部变量表是一组变量值的存储空间,用于存储方法参数和方法内部定义的局部变量。在java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了方法所需要分配的局部变量表的最大容量。
局部便链表的容量以变量槽为基本单位,除了double和Long都占用1个槽(他俩占用两个槽)。槽可以复用。
操作数栈
操作数栈的深度也在编译期就确定好了。在对操作数进行操作之前都要先放入操作数栈。例如在做运算操作时,先要将操作数压入操作数栈顶后,调用运算指令来进行。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转换成直接引用,这种转换被称为静态解析。另外一部分在每次运行时都转化成直接引用,这部分就称为动态连接。
方法返回地址
当一个方法开始执行后有两种方式推出这个方法,第一种为return语句。另一种为抛出异常。无论采用哪种方式,在方法退出后都必须返回到最初方法被调用的地方。如果是正常退出的话,栈帧中很可能保存着PC计数器,这个程序计数器就可以作为方法的返回地址。
如果是异常退出,那就要根据异常表来确定返回地址了。
附加信息
虚拟机可以将一些规范里没有描述的信息增加到栈帧中。例如与调试、性能收集相关的信息。这部分信息取决于具体虚拟机的实现。
方法调用
方法调用不等于方法中的代码被执行,而是要确定被调用的方法版本。
解析
如果方法在程序运行之前,就有一个可确定的调用版本,那么在类加载的解析阶段,会将方法的符号引用直接转换成直接引用。这类方法的调用被称为解析。
在java中符合编译器可知,运行期不可变这个要求的方法,有 static方法 final方法 private方法,父类方法,构造器方法。这几个方法被称为非虚方法,其他的方法被称为虚方法。
解析调用一定是个静态的过程,在编译期间就能完全确定,在类加载的解析阶段就会把设计的符号引用全部转变为明确的直接引用。
分派
分派调用过程将会揭示多态性特征的一些最基本的体现,如重写和重载
静态分派
public class Main1 {
static class Human {
}
static class Father extends Human{
}
static class Son extends Human{
}
public void who(Human human){
System.out.println("i am human");
}
public void who(Father father){
System.out.println("i am father");
}
public void who(Son son){
System.out.println("i am son");
}
public static void main(String[] args) {
Main1 main1 = new Main1();
Human father = new Father();
Human son = new Son();
main1.who(son);
main1.who(father);
}
}
以其中的
Human father = new Father();
语句为例
Human被称为静态类型或者外观类型,Father被称为实际类型或者运行时类型。
静态类型和实际类型在程序中都有可能发生变化。,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改编。
并且最终的静态类型在编译期是可知的。而实际类型变化的结果在运行期才可确定。
在重载时,是通过参数的静态类型而不是实际类型确定的。
所有依赖静态类型来决定方法执行版本的分派动作被称为静态分派。静态分派最经典的应用场景就是重载。静态分派发生在编译阶段。
动态分派
动态分派和重写有着很密切的关系。有多态性的是方法,而字段没有多态性。
public class Main1 {
static class Father {
int age;
public Father(){
age = 40;
show();
}
public void show(){
System.out.println("father is "+age+" age");
}
}
static class Son extends Father{
int age;
public Son(){
age = 10;
show();
}
public void show(){
System.out.println("son is "+age+" age");
}
}
public static void main(String[] args) {
Father son = new Son();
son.show();
}
}
动态分派首先要确定参数的实际类型,然后根据实际类型查找要执行的方法版本。
解释执行
hotsport虚拟机是基于栈的解释执行。