栈是一种FILO类型的数据结构。在虚拟机内存中有两个栈,一个是虚拟机栈,一个是本地方法栈。其中虚拟机栈是用来执行Java执行代码的。而本地方法栈则是为虚拟机使用到的Native方法服务。关于本地方法栈,此处不做展开。主要描述虚拟机栈。我们平时提到的栈也就是指虚拟机栈。
1.特点
- 栈描述的是java代码执行的一种内存模型。
- 栈中包含的元素为栈帧(一个栈帧对应一个方法)
- 栈是线程独立的
2.栈帧
栈帧(Stack Frame)是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack) 的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。典型的栈帧结构如图:
2.1 局部变量表
用于存放方法参数和方法内部定义的局部变量。该表在编译期由Code属性中的max_locals确定局部变量表的大小。局部变量表的容量以变量槽(Variable Slot)为最小单位。
- 局部变量表中第0位索引的Slot默认
“this”
关键字的引用。 - 该表中存放三种变量:
- 基本数据类型
- 引用类型
- returnAddress类型(是指向特定指令内存地址的指针)
2.2 操作数栈
方法真正执行的地方
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的
2.3 动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
2.4 返回地址
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
3.动态链接详解
3.1 方法调用
讲解动态链接前先介绍一下方法的调用
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
与之相对应的是,在Java虚拟机里面提供了5条方法调用字节码指令,分别如下:
- invokestatic:调用静态方法。
- invokespecial:调用实例构造器方法、私有方法和父类方法。
- invokevirtual:调用所有的虚方法。还有final修饰的方法。
- invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而- - invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
3.2 方法分派
方法分派分为静态单分派、静态多分派、动态单分派、动态多分派4种。对应java 中的重载和重写。
静态分派
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);
}
}
运行结果如下:
将上面代码中稍作改动:
sr.sayHello((Man)man);
sr.sayHello((Woman)woman);
运行结果如下:
编译器在重载时是通过参数的静态类型(Human)而不是实际类型(Man或Woman)作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。
动态分派
动态分派主要用来实现java多态的一个另一个特性:重写(Override)。
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指令的运行时解析过程大致分为以下几个步骤:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
单分派与多分派
两者的区别在于我们确定所要执行的方法的判断依据的数量是一个还是多个。
当我们调用重载方法时,我们需要依据方法调用者的声明类型和方法的参数来确定我们要调用的方法
当我们调用重写方法时,我们的判断依据只有调用者实际的类型。