栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定。并且写入到了方法表的Code属性之中,因此,一个栈需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
一个线程中方法的调用链可能会很长,很多方法同时处于执行状态,对于执行引擎来说,只有栈顶的栈帧是有效的。
- 局部变量表——建立在堆栈上,线程私有
- 操作数栈
- 动态连接
- 方法调用
但为什么会选择执行参数为Human的重载呢?在这之前,先按如下代码定义两个重要的概念: Human man = new Man();
上面代码中的“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')。 动态分配:
我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。