JVM–方法调用

方法调用不是方法执行,方法调用是让jvm确定调用哪个方法,所以,程序运行时的它是最普遍、最频繁的操作。jvm需要在类加载期间甚至运行期间才能确定方法的直接引用。

解析

所有方法在Class文件都是一个常量池中的符号引用,类加载的解析阶段会将其转换成直接引用,这种解析的前提是:要保证这个方法在运行期是不可变的。这类方法的调用称为解析。

jvm提供了5条方法调用字节码指令:

  • [ ] invokestatic:调用静态方法
  • [ ] invokespecial:调用构造器方法<init>、私有方法和父类方法
  • [ ] invokevirtual:调用所有的虚方法。
  • [ ] invokeinterface:调用接口方法,会在运行时期再确定一个实现此接口的对象
  • [ ] invokedynamic: 现在运行时期动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条指令,分派逻辑都是固化在虚拟机里面的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。InvokeDynamic指令详细请点击InvokeDynamic指令

invokestaticinvokespecial指令调用的方法,都能保证方法的不可变性,符合这个条件的有静态方法私有方法实力构造器父类方法4类。这些方法称为非虚方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
    public static void main(String[] args) {
        //invokestatic调用
        Test.hello();
        //invokespecial调用
        Test test = new Test();
    }
    static class Test{
        static void hello(){
            System.out.println("hello");
        }
    }
}
JVM--方法调用

解析调用一定是一个静态的过程,在编译期间就可以完全确定,在类装载的解析阶段就会把涉及的符号引用全部转化为可确定的直接引用,不会延迟到运行期去完成。而分派调用可能是静态的也可能是动态的,根据分派一句的宗量数可分为单分派和多分派。因此分派可分为:静态单分派、静态多分派、动态单分派、动态多分派。

静态分派(方法重载)

所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {
    static class Phone{}
    static class Mi extends Phone{}
    static class Iphone extends Phone{}
 
    public void show(Mi mi){
        System.out.println("phone is mi");
    }
    public void show(Iphone iphone){
        System.out.println("phone is iphone");
    }
    public void show(Phone phone){
        System.out.println("phone parent class be called");
    }
 
    public static void main(String[] args) {
        Phone mi = new Mi();
        Phone iphone = new Iphone();
 
        Test test = new Test();
        test.show(mi);
        test.show(iphone);
        test.show((Mi)mi);
    }
}

执行结果:

1
2
3
phone parent class be called
phone parent class be called
phone is mi

我们把上面代码中的Phone称为变量的静态类型或者叫外观类型,吧MiIphone称为实际类型,静态类型仅仅在使用时发生变化,编译可知;实际类型在运行期才知道结果,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

所以,jvm重载时是通过参数的静态类型而不是实际类型作为判定依据。下图可以证明:

JVM--方法调用

根据上面的代码也可以看出,我们可以使用强制类型转换来使静态类型发生改变。

动态分派(方法覆盖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test2 {
    static abstract class Phone{
        abstract void show();
    }
    static class Mi extends Phone{
        @Override
        void show() {
            System.out.println("phone is mi");
        }
    }
    static class Iphone extends Phone{
        @Override
        void show() {
            System.out.println("phone is iphone");
        }
    }
 
    public static void main(String[] args) {
        Phone mi = new Mi();
        Phone iphone = new Iphone();
        mi.show();
        iphone.show();
        mi = new Iphone();
        mi.show();
    }
}
1
2
3
phone is mi
phone is iphone
phone is iphone

这个结果大家肯定都能猜到,但是你又没有想过编译器是怎么确定他们的实际变量类型的呢。这就关系到了invokevirtual指令,该指令的第一步就是在运行期确定接受者的实际类型。所以两次调用invokevirtual指令吧常量池中的类方法符号引用解析到了不同的直接引用上。

JVM--方法调用

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

(1)找到操作数栈顶的第一个元素(对象引用)所指向的对象的实际类型,记作C;
(2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError。
(3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证。
(4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

动态类型语言支持

动态语言的关键特征是它的类型检查的主体过程是在运行期间而不是编译期。相对的,在编译期间进行类型检查过程的语言(java、c++)就是静态类型语言。

运行时异常:代码只要不运行到这一行就不会报错。
连接时异常:类加载抛出异常。

那动态、静态类型语言谁更好?

它们都有自己的优点。静态类型语言在编译期确定类型,可以提供严谨的类型检查,有很多问题编码的时候就能及时发现,利于开发稳定的大规模项目。动态类型语言在运行期确定类型,有很大的灵活性,代码更简洁清晰,开发效率高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MethodHandleTest {
    static class ClassA {  
        public void show(String s) {
            System.out.println(s);  
        }  
    }  
    public static void main(String[] args) throws Throwable {  
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();  
        // 无论obj最终是哪个实现类,下面这句都能正确调用到show方法。
        getPrintlnMH(obj).invokeExact("fantj");
    }  
    private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
        // MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)。   
        MethodType mt = MethodType.methodType(void.class, String.class);
        // lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。   
        // 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者,也即是this指向的对象,这个参数以前是放在参数列表中进行传递,现在提供了bindTo()方法来完成这件事情。   
        return lookup().findVirtual(reveiver.getClass(), "show", mt).bindTo(reveiver);
    }  
}
fantj

无论obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),都可以正确调用到show()方法。

仅站在Java语言的角度看,MethodHandle的使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别

  • ReflectionMethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的三个方法findStatic()findVirtual()findSpecial()正是为了对应于invokestaticinvokevirtual & invokeinterfaceinvokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。
  • Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection重量级,而MethodHandle轻量级
  • 由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。
  • MethodHandleReflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。
invokedynamic指令

参考原文:https://blog.csdn.net/a_dreaming_fish/article/details/50635651

一开始就提到了JDK 7为了更好地支持动态类型语言,引入了第五条方法调用的字节码指令invokedynamic,但前面一直没有再提到它,甚至把之前使用MethodHandle的示例代码反编译后也不会看见invokedynamic的身影,它到底有什么应用呢?

某种程度上可以说invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有四条invoke*指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。而且,它们两者的思路也是可类比的,可以想象作为了达成同一个目的,一个用上层代码和API来实现,另一个是用字节码和Class中其他属性和常量来完成。因此,如果前面MethodHandle的例子看懂了,理解invokedynamic指令并不困难。
每一处含有invokedynamic指令的位置都被称作“动态调用点(Dynamic Call Site)”,这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法上。我们还是照例拿一个实际例子来解释这个过程吧。如下面代码清单所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class InvokeDynamicTest {
    public static void main(String[] args) throws Throwable {  
        INDY_BootstrapMethod().invokeExact("icyfenix");  
    }
    public static void testMethod(String s) {
        System.out.println("hello String:" + s);  
    }
    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {
        return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
    }
    private static MethodType MT_BootstrapMethod() {  
        return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null);
    }
    private static MethodHandle MH_BootstrapMethod() throws Throwable {
        return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());  
    }
    private static MethodHandle INDY_BootstrapMethod() throws Throwable {  
        CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), "testMethod", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null));  
        return cs.dynamicInvoker();  
    }
}
hello String:icyfenix

BootstrapMethod(),它的字节码很容易读懂,所有逻辑就是调用MethodHandles$Lookup的findStatic()方法,产生testMethod()方法的MethodHandle,然后用它创建一个ConstantCallSite对象。最后,这个对象返回给invokedynamic指令实现对testMethod()方法的调用,invokedynamic指令的调用过程到此就宣告完成了。

发表评论