运行时栈帧结构

栈帧是虚拟机进行方法调用和方法执行的数据结构,栈帧存储了方法的局部变量表,操作数栈,动态链接和方法返回地址等信息。位于栈顶的栈帧成为当前栈帧,执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

局部变量表

是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。虚拟机使用局部变量表完成参数值到参数变量列表的传递过程,如果执行的是方法实例方法,那么局部变量表的第0位存储是this指针。为了尽可能节省栈空间,局部变量表中的Slot可以重用,如果pc计数器的值已经超出摸个变量的作用域,这个变量对应的Slot就可以交给其他变量使用。

某些情况下Slot的复用会直接影响到系统的来讲收集:

public class LocalVarTest {
    public static void main(String[] args) {
        {
            byte[] t = new byte[64*1024*1024];
        }
        System.gc();
    }
}

输出:
[GC (System.gc()) 71993K->67323K(125952K), 0.0016887 secs]
[Full GC (System.gc()) 67323K->67235K(125952K), 0.0086093 secs]

可以看到 上边申请的那 64M 虽然超出了作用域但是并没有被回收。

public class LocalVarTest {
    public static void main(String[] args) {
        {
            byte[] t = new byte[64*1024*1024];
        }
        int a  = 0;
        System.gc();
    }
}

输出:
[GC (System.gc()) 71996K->67305K(125952K), 0.0108727 secs]
[Full GC (System.gc()) 67305K->1699K(125952K), 0.0123909 secs]

从 full gc 中可以看出64M 的byte数组已经被回收了。

造成这个的原因就是复用,虽然超出了 t 的作用域但是,因为局部变量表中仍然存在引用,所以并没有被回收。所以如果存在很大的对象不再使用我们可以手动设置为null,但是如果经过JIT编译成本地代码之后就不会出现上述的问题了。

另外局部变量表不会给变量赋默认值,所以不能不赋值就直接使用(类变量会赋默认值,包括静态和非静态变量都会赋值)

方法调用

方法调用阶段唯一的任务是确定被调用方法的版本,暂时还不涉及方法内部的具体运行过程。

解析

java中有五条方法调用字节码指令,分别为:

  • invokestatic 调用静态方法
  • invokespecial 调用实例构造器,私有方法,父类方法。
  • invokevirtual 调用所有的虚方法。
  • invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象。
  • invokedynamic 在运行时动态解析调用点限定符所引用的方法,然后再执行该方法。

分派

  1. 静态分派

    Human man = new Man();

上面的代码中 Human 称为变量的实际类型或者外观类型,Man 称为实际类型。静态类型编译期间可知,实际类型只有等到运行期间才确定。

编译器在重载时是通过参数的静态类型而不是实际类型作为判断的。so,编译期间就能确定调用哪个重载版本。

  1. 动态分派
    静态分派与重载联系密切,动态分派则与重写密不可分。

在 invokevirtual 多态查找过程中,先从实例类型对象的类开始查找,如果找到并且能调用则查找结束。如果找不到按照继承关系从下往上依次对C的各个父类查找。

java语言是一种静态多分派,动态单分派的语言。

动态语言与静态语言

静态语言在编译期间就能确定完整的符号引用,符号引用能明确的指定调用哪个方法。

invokevirtual #13                 // Method PrintClass:(Lcom/acyouzi/jvm/OverwriteTest$A;)V

动态语言在编译期间最多只能确定方法名称,参数,返回值,不回去确定方法所在的具体类型,方法的接受者不确定,只有在运行时才能确定。