栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定。并且写入到了方法表的Code属性之中,因此,一个栈需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
一个线程中方法的调用链可能会很长,很多方法同时处于执行状态,对于执行引擎来说,只有栈顶的栈帧是有效的。
- 局部变量表——建立在堆栈上,线程私有
一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。以变量槽(slot)为基本单位。每个slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,其中reference是对象的引用,可以查到对象在java堆中的起始地址索引和方法区中的对象数据类型。returnAddress是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
虚拟机通过索引定位的方式使用局部变量表,若是32位数据,索引n代表使用第n个slot,若是64位,则使用第n和n+1个slot。
- 操作数栈
- 动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。
Class文件的常量池中有你大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接。
- 方法调用
方法调用阶段唯一的任务就是确定被调用方法的版本(即调用了哪一个方法)。Class文件的编译过程不包含传统编译的连接步骤,所有方法调用中的目标方法在Class文件里面存储的都是一个常量池中的符号引用,而不是方法在实际运行时内存布局中的入口地址。这一特性给java带来了更加强大的动态扩展能力。
解析:
调用目标在程序代码写好、编译器进行变异的时候就必须确定下来,这类方法的调用称为解析。在java中符合“编译期可知、运行期不可变”的方法有静态方法和私有方法,两者都不可能通过继承或别的方式重写出其他版本,因此他们都适合在类加载阶段进行解析。
分派:
静态分派:
但为什么会选择执行参数为Human的重载呢?在这之前,先按如下代码定义两个重要的概念:
Human man = new Man();
上面代码中的“Human”称为变量的静态类型(Static Type)或者外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是编译期可知的;而实际类型变化的结果在运行期才可确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么?如下面的代码:
上面代码中的“Human”称为变量的静态类型(Static Type)或者外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是编译期可知的;而实际类型变化的结果在运行期才可确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么?如下面的代码:
解释了这两个概念,再回到上述代码中。main()里面的两次sayHello()方法调用,在方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数和数据类型。代码中刻意定义了两个静态类型相同,实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型在编译期是可知的,所以在编译阶段,Javac编译器就根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法的两条invokevirual指令的参数中。
所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动力实际上不是由虚拟机来执行的。
编译器虽然能确定出方法的重载版本,但是很多情况下,这个重载版本并不是“唯一的”,往往只能确定一个“更适合的”版本。如sayHello('a');找重载方法时顺序:sayHello(char arg) --> sayHello(int arg) --> sayHello(long arg)
--> sayHello(float arg) --> sayHello(double arg) --> sayHello(Character arg) --> sayHello(Serializable arg)
--> sayHello(Object arg) --> sayHello(char... arg)
因为java.lang.Serializable是java.lang.Character实现的一个接口,同时java.lang.Character还实现了一个java.lang.Comparable<Character>接口,若同时出现两个参数分别为Serializable和Comparable<Character>的重载方法,他们的优先级相同,编译器无法为确定要转换为哪个类型,会提示模糊,拒绝编译。此时程序必须在调用时显示的制定字面量的静态类型,如sayHello((Comparable<Character>) 'a')。
动态分配:
我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。