运行时帧栈结构
帧栈是在虚拟机栈中的栈元素,每个帧栈包含局部变量表、操作数栈、动态连接、方法返回地址和一些额外信息。编译时,帧栈需要多大局部变量表,操作数栈多深都已确定,且分配了内存,不会受到运行期变量数据的影响。对执行引擎来说,活动线程中只有栈顶的栈帧是有效的,表示当前栈帧,其所关联的方法为当前方法。
l 局部变量表
1. 最小单位为Slot(一个32位以内的数据类型),有boolean,byte,char,short,int,float,reference,returnAddress这8种,reference为引用类型,returnAddress为一条字节码指令的地址。对64位数据类型,高位在前的方式分配两个Slot空间,如long、double(有非原子性协定,但由于帧栈为线程私有,不会发生并发安全问题)
2. 局部变量回收
不回收:
仍不回收:
回收了!:
因为placeholder即时作为局部变量,在作用域之外该局部变量表的Slot中仍会有对其的引用,直到有其他变量将其占用的Slot复用才会使placeholder无法被GC Roots到达,真正使placeholder被回收。
3. 局部变量必须赋初值才能使用
类变量在“准备”阶段就有初始值,而后在初始化时又会赋予程序定义的初始值。而局部变量没有“准备”阶段,因而局部变量必须赋初值后才能使用:
不过该问题会在编译期被检查出来。
l 操作数栈
存放指令中的操作。每次指令操作时向操作数栈入栈和出栈的过程。另外栈帧之间存在内容共享:如局部变量表共享和操作数栈共享。
l 动态连接
Class文件中有大量符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分在类的加载过程或第一次使用时直接引用(静态解析);另外一部分在每次运行期间转化为直接引用,这部分称为动态连接。
l 方法返回地址
方法退出后用于返回方法被调用的位置。
方法正常退出时:调用PC计数器的值就可返回地址,栈帧中很可能保存该计数值;
方法异常退出:返回地址通过异常处理器表来确定,栈帧中一般不会保存该值。
方法调用
方法调用不是方法执行,方法调用阶段唯一任务就是确定被调用方法的版本。
包括解析、分派(静态分派与动态分派)
l 静态分派
栗子:
输出:
虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据。且静态类型是编译期可知,实际类型是运行期可知,所以重载的方法版本是sayHello(Human)。所有依赖静态类型来定位方法执行版本的分派动作,称为静态分派。
l 动态分派
多态的体现,多态都懂的,父类对象被赋予了子类对象,调用父类中定义的方法时实际会去调用子类中Override(覆盖)的方法。
l 单分派与多分派
栗子:
结果:
结果毫无疑问,多分派是指:看看编译期选择过程,即静态分派过程,选择目标方法时的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360。这次选择产生了两条invokevirtual指令,两条指令参数分别为常量池中指向Father.hardChoice(_360)和Father.hardChoice(QQ)方法的符号引用,因此是根据两个宗量选择的,所以Java的静态分派属于多分派类型
单分派:在执行son.hardChoice(QQ)时, 由于编译期已决定目标方法是hardChoice(QQ),所以运行期只关心接收者的实际类型是Son还是Father。因此只有一个宗量,所以Java的动态分派属于单分派类型。