栈帧stack frame是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法调用从开始执行至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class的时候,就在方法的Code属性的max_locals数据项中确定了改方法所需要分配的局部变量表的最大容量。
局部变量表的容量以标量槽(Variable Slot)为最小单位。虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表的最大Slot数量。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属实例的引用
,在方法中可以通过关键字“this”来访问隐含的参数,去与参数按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,在根据方法体内部定义的变量顺序和作用域分配其余的Slot。
也称为操作栈,是一个先入后出栈。同局部变量表一样,操作栈的最大深度也在编译的时候写入到Code属性max_stacks数据项中。
每个栈帧都包含一个指向运行时常量池中该帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
当一个方法开始执行后,只有两种方式可以退出这个方法。第一种是执行引擎遇到了任意一个方法返回的字节码指令,这时候可能会有返回值传递给上传的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令到决定,这种退出方法的方式称为正常完成出口。
另一种退出方式是,方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理。这种退出方式称为异常完成出口,一个方法使用异常完成出口的方式退出,是不会给它的上传调用者产生任何返回值的。
方法调用不等同于方法执行,方法调用阶段是确定调用方法的版本(即调用哪个方法),暂时不涉及方法内部的具体运行过程。
只要能被invokestatic, invokespeial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的由静态方法
、私有方法
、实例构造器
、父类方法
4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法。final方法是一种非虚方法。
这节主要讲Java 重写和重载在Java虚拟机之中如何实现的。先来看段代码
public class StaticDispatch{
static abstract class Human{}
static class Man extends Human{}
static class Woman extends Human{}
public void sayHello(Human guy){
sout("Hello, guy");
}
public void sayHello(Man guy){
sout("Hello, man");
}
public void sayHello(Woman guy){
sout("Hello, woman");
}
}
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
//结果
//hello,guy
//hello,guy
上面代码中的Human称为变量的静态类型,或者叫做外观类型。后面的Man称为变量的实际类型。
//静态类型 man = new 实际类型
Human man = new Man();
main()里面的两次sayHello方法调用,在方法接受者已经确定是sr的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中刻意定义了两个静态类型相同但实际类型不同的变量(man和woman),但虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的
。并且静态类型是编译器可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型觉得使用哪个重载版本
,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main方法里的两条invokevirtual指令的参数中。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派典型的就是方法的重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机执行的。
动态分派和多态性的重写override有密切的关联。
public class TestCase{
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类的sayHello,但是被Man类重写了
man.sayHello(); // man say hello
Human woman = new Woman();
woman.sayHello(); // woman say hello
man = new Woman();
man.sayHello(); // woman say hello
}
}
javap -v获取其字节码如下所示
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
// new 对象,创建一个引用
0: new #2 // class com/jian8/basic/TestCase$Man
//复制栈顶元素,即复制引用
3: dup
// 编译时方法绑定调用方法。init方法
4: invokespecial #3 // Method com/jian8/basic/TestCase$Man."":()V
// 将操作栈栈顶引用类型保存到局部变量表
7: astore_1
// 局部变量中的引用类型保存到操作栈中
8: aload_1
/**
* invokevirtual 运行时方法绑定调用方法。
* 指令的解析大致分为以下步骤:
* 1. 找到操作数栈栈顶的第一个元素所指向的对象实际类型,记为C
* 2. 如果在C中找到与常量中的描述符和简单名称都相符的方法,则进行校验,通过返回这个方法的直接引用。查找结束
* 3. 不通过,返回IllegalAccessError异常。按照继承关系从下往上一次对C的各个父类进行步骤2中的搜索和验证过程。
* 4. 如果始终没找到合适的方法,抛出AbstractMethodError异常
* invokevirtual指令执行的第一步就是在运行期间确定接收者实际的类型,
* 所以两次调用invokevirtual指令把常量池中的类方法符号引用解析到不同的直接引用上,
* 这就是Java重写的本质。
* 这里第1步确定元素指向的是Man类型
*/
9: invokevirtual #4 // Method com/jian8/basic/TestCase$Human.sayHello:()V
12: new #5 // class com/jian8/basic/TestCase$Woman
15: dup
16: invokespecial #6 // Method com/jian8/basic/TestCase$Woman."":()V
19: astore_2
20: aload_2
// 确定元素指向的是Woman类型
21: invokevirtual #4 // Method com/jian8/basic/TestCase$Human.sayHello:()V
24: new #5 // class com/jian8/basic/TestCase$Woman
27: dup
28: invokespecial #6 // Method com/jian8/basic/TestCase$Woman."":()V
31: astore_1
32: aload_1
// 确定元素指向的是Woman类型
33: invokevirtual #4 // Method com/jian8/basic/TestCase$Human.sayHello:()V
36: return
变量man在两次调用中执行了不同的方法,导致这个现象的原因很明显,是两个变量的实际类型不同。
public class TestCase{
static class QQ{}
static class Wechat{}
public static class Father{
public void hardChoice(QQ arg){
System.out.println("father chooses qq");
}
public void hardChoice(Wechat arg){
System.out.println("father chooses Wechat");
}
}
public static class Son extends Father{
@Override
public void hardChoice(QQ arg){
System.out.println("Son chooses qq");
}
@Override
public void hardChoice(Wechat arg){
System.out.println("Son chooses Wechat");
}
}
public static void main(String[] args){
Father father = new Father();
Son son = new Son();
father.hardChoice(new Wechat()); //father chooses Wechat
son.hardChoice(new QQ()); // Son chooses qq
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class com/jian8/basic/TestCase$Father
3: dup
4: invokespecial #3 // Method com/jian8/basic/TestCase$Father."":()V
7: astore_1
8: new #4 // class com/jian8/basic/TestCase$Son
11: dup
12: invokespecial #5 // Method com/jian8/basic/TestCase$Son."":()V
15: astore_2
16: aload_1
17: new #6 // class com/jian8/basic/TestCase$Wechat
20: dup
21: invokespecial #7 // Method com/jian8/basic/TestCase$Wechat."":()V
24: invokevirtual #8 // Method com/jian8/basic/TestCase$Father.hardChoice:(Lcom/jian8/basic/TestCase$Wechat;)V
27: aload_2
28: new #9 // class com/jian8/basic/TestCase$QQ
31: dup
32: invokespecial #10 // Method com/jian8/basic/TestCase$QQ."":()V
35: invokevirtual #11 // Method com/jian8/basic/TestCase$Son.hardChoice:(Lcom/jian8/basic/TestCase$QQ;)V
38: return
在main函数中调用了两次hardChoice方法,我们来看看编译阶段编译器的选择过程,也就是静态分配的过程
。这时候选择目标方法依据有两点:一是静态类型
是Father还是Son,二是方法参数
是QQ还会Wechat。这次选择的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向Father.hardChoice(Wechat)
及Father.hardChoice(QQ)
方法的符号引用
。
再看看运行阶段虚拟机的选择,也就是动态分配的过程。在执行son.hardChoice(new QQ())
时,更准确地说是执行住代码对应的invokevirtual
指令时,由于编译器已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不关心传递过来的参数QQ到底是腾讯QQ还是奇瑞QQ,因为这时参数的静态类型、实际类型都堆方法不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接受者的实际类型是Father还是Son。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同的方法的地址入口是一致的,都指向父类的实现入口地址
。上图中,Son重写了来自Father的全部方法,因此Son的方法没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以他们的方法表中所有从Object继承的方法都指向了Object的数据类型。