栈帧
jvm以方法作为最基本的执行单元,栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
栈帧需要分配的内存只取决于程序源码和虚拟机的内存布局,不受程序运行时的变量影响,因为栈帧所需要多大的局部变量表和操作数栈在编译源码时已经被分析出来了,并写到了方法表的code属性中。
只有位于栈顶的栈帧才是真正运行的,被称为当前栈帧,该方法被称为当前方法。
局部变量表
用于存放方法参数和方法内部定义的局部变量,表的容量在编译源码时已经确定
表中每个变量槽存放的类型有int、byte、boolean、short、char、float、reference、returnAddress八种类型。
reference表示对一个对象实例的引用,即可以根据reference找到对象的地址,并且可以找到这个对象所属类的类型信息,即对象章节那里的reference。
而returnAddress指向字节码指令的地址,现在很少用了。
局部变量表每个槽可以存放32位及以下数据,对于long和double这种64位的类型用分割存储的方式放在两个槽中,并且局部变量表是线程私有的,所以不会产生线程安全问题。
局部变量表的第0位索引的变量槽默认是用于传递方法所属对象实例(相当于代码中的this)的引用,其余参数则排在其后面,即以索引1开始。
如果局部变量没有赋初始值,那么这个变量是不可用的,因为它不像类变量(static修饰)那样有类加载的准备阶段,也不像对象那样在分配完内存后有个赋零值操作
操作数栈
操作数栈的最大深度也是编译源码时就确定了的,并且放在方法表的code属性中
操作数栈的元素是java的数据类型,例如int、long、char等,32位占一个栈容量,64占两个栈容量。
方法开始时操作数栈为空,在执行字节码将操作数进栈和出栈,例如iadd这个字节码操作,就会将两个最接近栈顶的元素出栈相加,然后将结果进栈。
栈的元素类型必须要和字节码指令匹配,例如iadd表示只能用int,操作数栈出栈的两个元素只能是int类型。
动态连接
每个栈帧都包含了指向运行时常量池中栈帧所属方法的引用(符号引用)
静态解析:符号引用在类加载阶段或者第一次使用时会转化成直接引用。
动态连接:符号引用在运行期间转化成直接引用。
方法返回地址
方法正常退出时栈帧保存主调方法的pc计数器的值作为返回地址
方法有两种方式退出
- 执行引擎遇到方法返回的字节码(相当于return)
- 方法执行时遇到了异常
方法无论以哪种方式退出,都必须返回最初方法被调用的位置,程序才能继续执行,一般来说,方法正常退出时,主调方法的pc计数器的值是方法返回地址。异常退出时返回地址保存在异常处理器中。
方法退出之后,恢复上层方法的局部变量表和操作数栈,将返回值压入操作数栈中,调用pc计数器的下一条指令。
方法调用
方法调用的意思是确定调用哪一个方法,方法调用在Class文件里存储的是符号引用而不是直接引用,即所有方法调用的目标方法都是符号引用,直到类加载阶段或者是运行阶段才会转化成直接引用
解析
方法的调用称为解析,调用的方法在编译器编译那一刻已经确定下来了。并且能在类加载阶段就把符号引用解析为直接引用,不需要等到运行阶段。
在类加载阶段就把符号引用解析为直接引用的叫做非虚方法,有五种:
- 静态方法
- 私有方法
- 构造方法
- 父类方法
- fianl修饰的方法
分派
静态分派
静态分派意思是方法执行版本依靠静态类型。例如重载。
虚拟机在重载时是根据参数的静态类型而不是实际类型决定的,在调用重载方法时,根据静态类型选择对应参数的重载方法。
静态类型与实际类型:Human human = new man();
静态类型:Human是静态类型,在编译期可知
实际类型:human是实际类型,在运行期才可知
动态分派
运行期根据实际类型确定方法执行版本的分派过程称为动态分派。例如多态和重写。
动态分派是虚方法才有的,因为方法是运算时才加载。
注意:动态分派只对方法有效,对字段无效。即字段不参与多态,对象的静态类型属于哪个类,字段就属于哪个类。原因是虚方法的invokevirtual字节码指令字段不能使用。
在方法区中有一个虚方法表,虚方法表中存放各个方法的实际入口地址。
单分派和多分派
宗量:方法的接收者和方法的参数叫做宗量
- 单分派根据一个宗量对目标方法进行选择
- 多分派根据多个宗量对目标方法进行选择
静态分派属于多分派,因为重载时编译阶段需要根据静态类型和参数两个宗量进行选择方法
动态分派属于单分派,因为动态分派是在运行期选择方法的,只需要关注方法的调用者的实际类型。
动态类型语言
动态类型语言就是类检查的过程是在运行期而不是编译期进行。