运行时栈帧
每一个方法从调用开始到执行完成都对应着一张栈帧的进栈和出栈。栈帧中存储着局部变量表,操作数表,动态链接和方法返回地址。位于虚拟机最顶层的称为当前方法栈。
局部变量表
储存当前方法的局部变量和参数,局部变量表的容量以变量槽slot(32位)为存储单位。对于64位的数据,通过连续分配两个slot高位对齐的方式储存,由于局部变量表表是线程私有的,所以连续读取两个slot不会引起安全问题。
局部变量表的计数从1开始,第0位默认为“this”指针。
为了节省栈空间,局部变量的slot是可重用的。当某个slot的作用域没有覆盖全部的方法体,且程序计数器已经超出了该slot的作用域,则该slot可以被复用。
如果一个无用的局部变量占用了大量内存,那么设置一个a=0;的可以让垃圾回收器将其立即回收,这一点可以在某些极端情况下,用作对象内存过大,方法栈帧长时间不能回收,和方法调用次数倒不大jit的编译水平时的奇技。
局部变量和类属性不同。类属性存在一个准备阶段,即类属性在遇到一个new操作时,首先会检查这个类有没有被加载,如果加载完成,就通过“指针碰撞”或者“空闲列表法”的方式为其在堆中分配内存,为了解决指针碰撞可能产生的线程不安全问题,堆中会为每一个对象预留一块tlab,只有tlab满要使用其他区域的内存时才需要通过其他方式来保证线程安全。例如上锁。那么在前述的tlab或者分配内存的过程中,虚拟机会为所有变量附上默认值,但是,局部变量是无法享受这样的待遇的,所以局部变量不赋初值会导致编译不通过。
操作数栈
存放局部变量运算过程中的临时变量,初始位0,可以另一个栈帧的局部变量表共享数据以提高效率。
动态链接和方法返回地址
动态链接支持方法调用,可以分为静态解析和动态链接
方法返回,相当于当前方法的栈帧出栈。执行的操作有:恢复上层栈帧的局部变量和操作数栈,将返回值赋值到上层操作数栈中,调整pc计数器指向后一条指令。
方法调用
方法调用不等同于方法执行。方法调用是程序运行中最普遍最频繁的操作,由于Class文件中的方法调动依靠的是符号引用(即只是抽象层面的引用,被没有指向实际调用方法的内存地址),所以将符号引用转为直接引用的过程实际要到加载其中,甚至运行期间才能被确定下来。这为java语言的动态实现提供了极大的灵活性,同时也增加了程序设计的复杂性
解析
解析阶段会将一部分符号引用转化为直接引用。转化的前提是:方法在编译期间就有一个确定的调用版本,且在运行期间不会改变。这类的方法可以统称为非虚方法,包括:静态方法,私有方法,实例构造器和父类方法,以及final修饰的方法。
分派
分派的调用可能是静态的也可能是动态的,根据分派依据的宗量数可以分为:静态单分派,动态单分派,静态多分派和动态多分派。
静态分派
重载!所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。静态的典型应用是重载,静态分派的类型在编译阶段就已经确定下来了。此外,还要理解编译器确定一个更合适的版本时的模糊判断(例如:char-int-long-float-double-包装类-序列化-object)。
动态分派
重写!假设一个类A被两个子类BC重写了方法m,则虚拟机的执行过程为:将BC两个引用(也成为接受者)压入栈,然后分别调用invokevirtual指令来调用方法。其中,在invokevirtual执行的过程中,第一步就是查找操作数栈顶第一个元素的类型,由于两次的操作数栈顶分别是AB,所以invokevirtual调用的方法就是被重写了的方法。这就是重写的本质
单分派与多分派
宗量:方法接受者和方法参数的总称。单分派根据一个宗量对目标方法进行选择,多宗量根据多个宗量对目标方法进行选择。在jdk1.7以前java是一门静态多宗量,动态单宗量的语言。
基于栈的指令集和基于寄存器的指令集
基于寄存器的指令集执行速度更快。
基于栈的指令集可移植性更好,代码更紧凑,但是由于大量进出栈指令的原因,效率相对基于寄存器的指令集也更低。