深入理解JVM的方法调用


文章目录

  • 深入理解JVM的方法调用
  • 方法如何获得调用对应的地址


要想调用一个方法,就需要知道调用方法的地址。在java中,获取方法的地址的方式不是统一的。在java中,字节码执行调用方法的指令总共有5种。

invokestatic。用于调用静态方法。

invokespecial。用于调用实例构造器<init>()方法、私有方法和父类中的方法。

invokevirtual。用于调用所有的虚方法。

invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。

invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引 导方法来决定的。

静态分派:在代码编译期确定重载版本。

动态分派:在运行期根据 际类型确定方法执行版本。

invokevirtual指令的运行时解析过程大致分为以下几步:

1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。

2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。

3)否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。

4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

注意:需要说明的是,静态分派和动态分派不是二选一的关系,因为两者发生的时机就完全不同,静态分派是在编译阶段执行的,动态分派则是在运行期间执行的。同理,静态分派和类加载的解析也不是二选一的关系,典型的一个例子就是静态方法能够重写。

方法如何获得调用对应的地址

  1. 类加载阶段就可以获得方法地址(顺带说一下,方法地址一般指向方法区),主要体现在类的静态方法,构造方法,私有方法,和父类中的方法。其实也就是使用invokestaticinvokespecial这两种字节码指令的方法。这些方法会在类加载的解析阶段,就在常量池中把对应的符号引用替换成直接引用,也就是对应方法在内存中的地址。还有一个特殊的案例,就是被final修饰的方法,也会在类加载解析阶段替换直接引用,说它特殊主要是因为final修饰的方法底层使用的是invokevirtual字节码指令。
    能这么做的原因主要是可以在编译阶段就确定具体的方法版本。所以如果像普通的方法那样存在重载或者重写的问题,是不能够在解析阶段就确定具体的方法版本的。
  2. 通过动态分派获得方法地址。在运行期间调用某个虚方法的时候,会通过动态分派来获得方法的方法的地址。不过,由于动态分派的调用是十分频繁的,出于对性能的考虑,一般虚拟机会建立虚方法表,以此来维护具体执行一个方法对应的地址。方法表是在类加载的连接阶段(验证、准备、解析)进行初始化,准备了子类的初始化值后,虚拟机会把该类的虚方法表也进行初始化。

一个小问题的解释,之前我还在思考为什么要建立虚方法表,直接向静态变量那样替换常量池不就好了,方标,也节省空间。后来,仔细想想才发现,这根本就不可行。因为对于动态分派来说,可能存在两次调用的符号引用都是同一个,但是由于动态分派的存在,两次执行的实际代码其实根本就不一样,这不就产生冲突了吗。具体例子可以看《深入理解Java虚拟机》中关于动态分派的例子。