实战java虚拟机11-动态函数调用

实战java虚拟机
深入理解java虚拟机

动态类型语言
在JDK1.7中引入了一个新的invoke指令——invokedynamic,该指令的目的是为了更好的支持JAVA虚拟机平台上的动态语言,以及JAVA8中的lambda表达式。

什么是动态语言?动态语言的关键特征是它的类型检查的主体过程是在运行期而不是编译器。满足这个特征的语言有很多:APL,Erlang,Groovy,JavaScript,Lisp,Lua,Python,Ruby等。
相对的,在编译期就进行类型检查过程的语言(C++,JAVA)就是常用的静态语言


动态函数调用

  • 方法句柄(Method Handler): 方法句柄很像一个方法指针,或者代理。通过方法句柄就可以调用一个方法 。在class文件常量池中,有一项常量类型为CONSTANT_MethodHandle,这就是方法句柄。
  • 调用点(CallSite):调用点是对方法句柄的封装,通过调用点,可以获得一个方法句柄进行函数调用。使用调用点可以增强方法句柄的表达能力。
  • 启动方法(BootstrapMethods):通过启动方法可以获得一个调用点,获取调用点的目的是为了进行方法绑定和调用。
  • 方法类型(Method Type):用于描述方法的签名,比如方法的参数类型、返回值等。根据方法的类型,可以查找到可用的方法句柄。

方法句柄使用实例
在JDK7,引入了一些新的API来支持方法句柄的使用,这些类主要位于java.lang.invoke包中。

  • java.lang.invoke.MethodType:MethodType类提供了一组生成方法描述的API。使用其静态方法methodType(),可以根据返回值和参数类型,生成一个MethodType对象。
  • java.lang.invoke.MethodHandle:MethodHandle表示方法句柄。通过方法句柄,可以使用其invoke()方法,直接调用指定方法。
  • java.lang.invoke.MethodHandles:这是一个工具类,用于构造MethodHandle实例。
  • java.lang.invoke.MethodHanles.Lookup:这是MethodHandles的一个内部类,是一个工具类,用于查找和构造MethodHandle实例。

例:

public class SimpleMethodHandle {
    static class MyPrintln{
        //1.和java.io.PrintStream.Println()有相同的方法签名, (String)Void;
        protected void println(String s){
            System.out.println(s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = (System.currentTimeMillis() & 1L) == 0 ? System.out : new MyPrintln();
        System.out.println(obj.getClass().getName());


        //2.寻找方法句柄
        MethodHandle methodHandle = getPrintMethodHandler(obj);

        //3.方法类型绑定到实际的调用对象obj上,并执行。
        methodHandle.bindTo(obj).invoke("你好");
    }

    public static MethodHandle getPrintMethodHandler(Object receiver) throws NoSuchMethodException, IllegalAccessException {
        //2.1 构造方法签名实例
        MethodType mt = MethodType.methodType(void.class,String.class);

        //2.2 根据MethodType + 方法名 + 实际的对象类型,进行方法查找
        return MethodHandles.lookup().findVirtual(receiver.getClass(), "println", mt);
    }
}

对于Lookup对象查找函数时有三种方式:

  • findStatic():查找一个static方法,使用invokestatic进行函数调用
  • findVirtual():查找一个虚方法(实例方法),使用invokevirtual进行函数调用。
  • findSpecial :查找一个特殊方法,等价于使用invokespecial调用,用于访问私有方法、父类的方法。但是对于构造函数访问需要使用 findConstructor()方法。

java大部分都是虚函数,因此对于绝大多数方法使用findVirtual()方法查找就可以了。

调用findStatic()方法

//findStatic方法: Math. static double sin(double a)
MethodType sinMethodType = MethodType.methodType(double.class, double.class);
MethodHandle sinMethodHandle = MethodHandles.lookup().findStatic(Math.class, "sin", sinMethodType);

//static 方法不需要绑定调用对象
System.out.println("sin(30) = "+sinMethodHandle.invokeExact(Math.PI/2));


调用findSpecial() -- private方法

private void printLine(){
    System.out.println("call private printLn()");
}
 void specialMethodPrivate() throws Throwable {
    MethodHandle mh = MethodHandles.lookup().findSpecial(this.getClass(), "printLine", MethodType.methodType(void.class),this.getClass()).bindTo(this);
     mh.invokeExact();
}

调用findSpecial() -- Override方法

@Override
public String toString(){
    return "Override toString";
}

void specialMethodOverride() throws Throwable {
    MethodHandle mh = MethodHandles.lookup().findSpecial(Object.class, "toString", MethodType.methodType(String.class),this.getClass()).bindTo(this);
    System.out.println(mh.invoke());//调用Object.toString()

    mh = MethodHandles.lookup().findSpecial(this.getClass(), "toString", MethodType.methodType(String.class),this.getClass()).bindTo(this);
    System.out.println(mh.invoke());//调用Override toString
}

findSpecial()的参数,第1个参数为要调用方法的所在类,第2个参数为方法名,第3个为方法类型,第4个为实际调用对象。


调用点使用实例
调用点(CallSite)也是JDK17内新增的API,它也在java.lang.invoke包中。调用点用于包装方法句柄,通过一个调用点实例,就可以得到相关的方法句柄,从而实现方法调用。
这里写图片描述

  • 常量调用点(ConstantCallSite):调用目标不可变,一单绑定到了目标函数句柄上,就无法更改。
  • 可变调用点(MutableCallSite):调用目标可变,可以多次绑定到不同的目标函数句柄上,通过同一个调用点,就可以调用不同函数。
  • 易变调用点(Volatile):由于缓存等问题,可变调用点在多线程环境中,当一个线程更改了可变调用点的目标函数后,其他线程不能保证立即发现这个更改。它满足volatile语义

例:调用常量调用点(ConstantCallSite)

void constantCallSiteSample() throws Throwable {
    //返回值String, 参数(int ,int )
    MethodType methodType = MethodType.methodType(String.class, int.class, int.class);

    //寻找String.class的方法  String substring(int , int)
    MethodHandle methodHandle = MethodHandles.lookup().findVirtual(String.class, "substring", methodType);

    //构造 常量调用点
    ConstantCallSite ccs = new ConstantCallSite(methodHandle);
    MethodHandle invoker = ccs.dynamicInvoker();

    String result = (String) invoker.invoke("1234567890", 2, 4);
    System.out.println(result);
}

例:调用可变调用点(MutableCallSite)

void mutableCallSiteSample() throws Throwable {
    //返回值double, 参数(double)
    MethodType methodType = MethodType.methodType(double.class, double.class);

    //通过方法类型构造可变调用点, 注意这里没有通过 methodhandle构建callsite.
    MutableCallSite callSite = new MutableCallSite(methodType);

    //根据可变点生成MethodHandle,此时并没有绑定方法句柄,即没有可用函数
    MethodHandle invoker = callSite.dynamicInvoker();

    MethodHandles.Lookup lookup = MethodHandles.lookup();
    //查找方法句柄 Math.sin(double) ,Math.cos(double)
    MethodHandle sinHandler = lookup.findStatic(Math.class, "sin", methodType);
    MethodHandle cosHandler = lookup.findStatic(Math.class, "cos", methodType);

    //设置target,目标函数为Math.sin(double)
    callSite.setTarget(sinHandler);
    System.out.println("sin(90)="+invoker.invoke(Math.PI / 2));

    //设置target,目标函数为Math.cos(double)
    callSite.setTarget(cosHandler);
    System.out.println("cos(90)="+invoker.invoke(Math.PI / 2));
}

方法和反射句柄

public class ReflectionMain {
    public static final int COUNT = 1000000;

    int i = 0;

    public void increment() {
        i++;
    }

    //使用方法句柄调用
    public static void callByHandler() throws Throwable {
        ReflectionMain instance = new ReflectionMain();
        MethodType mt = MethodType.methodType(void.class);
        MethodHandle mh = MethodHandles.lookup().findVirtual(instance.getClass(), "increment", mt).bindTo(instance);
        long b = System.currentTimeMillis();
        for (int i = 0; i < COUNT; i++) {
            mh.invoke();
        }
        long e = System.currentTimeMillis();
        System.out.println(e - b);
    }

    //使用反射调用
    public static void callByReflection() throws Throwable {
        ReflectionMain instance = new ReflectionMain();
        Method method = instance.getClass().getMethod("increment", new Class[]{});
        long b = System.currentTimeMillis();
        for (int i = 0; i < COUNT; i++) {
            method.invoke(instance);
        }
        long e = System.currentTimeMillis();
        System.out.println(e - b);
    }
}

从本质上讲MethodHandle 和 Reflection机制都是在模拟方法调用,但是二者还是存在一些区别

  • Reflection是在Java代码层面的调用,而MethodHandle是在模拟字节码层面的方法调用。
  • Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息多。前者的方法在Java一端的全面映像,包含了方法的签名,描述符以及方法属性表中各种属性的Java端表示方式,还包含执行权限等运行期信息。而后者仅包含于该方法执行的信息。通俗的讲:Reflection是重量级,MethodHandle是轻量级。
  • 由于MethodHandle是对字节码的方法指令模拟的调用,所以理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但是目前实现还不完善)。而通过反射去调用方法则不行。

还有一点,Reflection API的设计目标只是为java服务的,而MethodHandle则设计成可服务于所有java虚拟机之上的语言。


指令invokedynamic使用实例
从某种程度上,invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决4条invokevirtual,invokespecial,invokestatic,invokeinterface指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转到具体用户代码中,让用户有更高的自由度。
而且,他们二者的设计思路也是可类比的,可以把它们想象成为了达成同一个目标,一个采用上层JAVA代码API来实现,一个用字节码和Class中其他属性、常量来完成。

每一处含有invokedynamic指令的位置都称作动态调用点(Dynamic Call Site),这条指令的第一个参数不再是方法符号引用的CONSTANT_Methodref_info常量,而是变成了JDK1.7新加入的CONSTANT_InvokeDynamic_info常量,从这个常量中可以得到3项信息:

  • 引导方法(Bootstrap Method):此方法存放在新增的BootstrapMethods属性中。引导方法是有固定参数的,且返回值是CallSite,代表真正要执行的目标方法调用。
  • 方法类型(MethodType)
  • 名称

根据CONSTANT_InvokeDynamic_info常量提供的信息,虚拟机可以找到并执行引导方法(Bootstrap
Method)
,从而获得一个CallSite对象,最终执行要执行的目标方法。

invokedynamic-掌控方法分配规则
invokedynamic与其他4条invoke*指令最大的区别就是它的分配逻辑不是由虚拟机决定的,而是有程序员决定的。
例:如何调用GrandFather.thinking(),打印i am grandfather

class GrandFather{
        void thinking(){
            System.out.println("i am grandfather");
        }
    }

    class Father extends GrandFather{
        @Override
        void thinking(){
            System.out.println("i am fater");
        }
    }

    class Son extends Father{
        @Override
        void thinking(){
            //如何调用GrandFather.thinking(),打印i am grandfather
        }
    }

如下:

 class Son extends Father{
        @Override
        void thinking(){
            //如何调用GrandFather.thinking(),打印i am grandfather

            MethodType mt = MethodType.methodType(void.class);
            try {
                //findSpecial 调用父类方法; 第1个参数为要调用方法的所在类,GrantFather
                MethodHandle mh = MethodHandles.lookup().findSpecial(GrandFather.class,"thinking",mt,this.getClass());
                mh.bindTo(this).invoke();
            } catch (Throwable e) {
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值