一、前言

  在了解了类加载的相关信息后,有必要进行更深入的学习,了解执行引擎的细节,如字节码是如何被虚拟机执行从而完成指定功能的呢。下面,我们将进行深入的分析。

二、栈帧

  我们知道,在虚拟机中与执行方法最相关的是栈帧,程序的执行对应着栈帧的入栈和出栈,所以栈帧对于执行引擎而言,是很重要的基础。栈帧的基本结构之前已经有所介绍,这里只是再简单的过一遍。

  栈帧主要包括了局部变量表、操作数栈、动态连接、方法返回地址等信息。

  2.1 局部变量表

  用于存放方法参数和方法内部的局部变量。局部变量表的大小在方法的Code属性中就已经定义好了,为max_locals的值,局部变量表的单位为slot,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型占用两个slot。注意,对于实例方法而言,索引为0的slot存放的是this引用,之后再依次存放方法参数,定义的局部变量;slot可以被重用,当局部变量已经超出了作用域时,在作用域外在定义局部变量时,可以重用之前的slot空间。同时,局部变量没有赋值是不能够使用的,这和类变量和实例变量是有不同的,如下面代码:

public void test() {
    int i;
    System.out.println(i);
}

  这样的代码是错误的,没有赋值不能够使用。

  2.2 操作数栈

  执行方法时,存放操作数的栈,栈的深度在方法的Code属性中已经定义好了,为max_stack的值,32位以内的类型占用一个栈单位,64为的类型占用两个栈单位。操作数栈可以与其他栈的局部变量表共享区域,这样可以共用一部分数据。

  2.3 动态连接

  动态连接是为了支持在运行期间将符号引用转化为直接引用的操作。我们知道,每一个方法对应一个栈帧,而每一个栈帧,都包含指向对应方法的引用,这个引用就是为了支持动态连接,如invokedynamic指令。动态连接与静态解析对应,静态解析是在类加载(解析阶段)或者第一次使用时将符号引用转化为直接引用,动态连接则是每一次运行的时候都需要进行转化(invokedynamic指令)。

  2.4 方法返回地址

  正常方法返回,返回地址为到调用该方法的指令的下一条指令的地址;异常返回,返回地址由异常表确定。方法返回时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC值。

三、方法调用

  在分析了栈帧后,我们接着分析方法调用,方法调用会导致栈帧入栈,而方法调用会确定调用哪一个方法,还不会涉及到具体的方法体执行。

  3.1 解析

  在程序执行前就已经确定了方法调用的版本,即编译期就确定了调用方法版本,这个版本在运行时是不可变的。静态方法、私有方法、final方法在编译时就可以确定具体的调用版本,静态方法直接与类型相关、私有方法在外部不可访问、final不可被继承,也可唯一确定,这些方法称为非虚方法,其他方法称为虚方法。在类加载的解析阶段就可以进行解析,如下方法调用在编译期就可以确定方法调用的版本。  

class Father {
    public static void print(String str) {
        System.out.println("father " + str);
    }
    
    private void show(String str) {
        System.out.println("father " + str);
    }
}

class Son extends Father {

}

public class Test {
    public static void main(String[] args) {
        Son.print("coder");
        //Father fa = new Father();
        //fa.show("cooooder");
    }
}
View Code

相关文章: