`
RednaxelaFX
  • 浏览: 3014966 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

通过代码简单介绍JDK 7的MethodHandle,并与.NET的委托对比

    博客分类:
  • Java
阅读更多
(注意:这篇blog里的信息只描述了发文时的情况。到JDK7正式发布的时候已经这篇文章里介绍的部分内容已经过时,包括MethodHandle的调用语法、java.dyn包改名为java.lang.invoke、该包的里API等。下面内容并未更新,敬请理解。)

JDK 7将会实现JSR 292,为在JVM上实现动态语言提供更多支持。其中,MethodHandle是JSR 292的重要组成部分之一。有了它,意味着Java终于有了引用方法的方式,或者用C的术语说,“函数指针”。(我差点要说“引用‘方法’的‘方法’”了,好pun)。
下面的讨论都是基于当前(2009-09)的设计而进行的,今后相关具体设计可能变化,但大的方向应该比较明确了。JDK 7的代码例子都是在JDK 7 Binary Snapshot build 70下测试的。执行程序时要添加-XX:+EnableMethodHandles参数。

与其说JDK 7的MethodHandle像C的函数指针,还不如说像.NET的委托。
C#与.NET从1.0版开始就有“委托”的概念,通过委托可以在代码中引用任意方法,无论方法的可访问性、所属类型如何,无论是静态还是实例方法。之前一帖也提到了,.NET的委托提供了为方法创建“别名”的能力,使我们可以用统一的方式去调用签名相同但名字和所属类型都不一定相同的方法。与C的函数指针所不同的是,.NET的委托不但引用了方法,还会引用执行该方法所需要的环境,对引用实例方法的委托来说“环境”就是方法所在的实例;而C的函数指针则仅指向函数的代码而已,没有引用环境的功能。而且,.NET的委托包含了足够的元数据,可用于运行时做类型检查;而C的函数指针则仅仅是个裸指针。调用委托的速度接近调用虚方法的速度。
JDK 7将引入的MethodHandle从许多方面说都与.NET的委托非常相似。MethodHandle也可以指向任意方法,提供为方法创建“别名”的能力;可以用统一的方式去调用MethodHandle。此外,MethodHandle还支持组合,可以以适配器的方式将多个MethodHandle串在一起,实现参数过滤、参数转换、返回值转换等许多功能。调用MethodHandle的速度接近调用接口方法的速度。
MethodHandle对许多JVM的内部实现来说并不是一个全新的概念。要实现JVM,在内部总会保留一些指向方法的指针。JDK 7只是把它(和其它许多JVM里原本就支持的概念)具体化为Java类型暴露给Java代码用而已;这就是所谓的“reification”。

好吧,简单介绍了些背景,下面就通过代码来认识和感受一下JDK 7的MethodHandle,并与.NET的委托对比。

第一组例子,照例上hello world:

JDK 7:
import java.dyn.*;
import static java.dyn.MethodHandles.*;

public class TestMethodHandle1 {
    private static void hello() {
        System.out.println("Hello world!");
    }
    
    public static void main(String[] args) {
        MethodType type = MethodType.make(void.class);
        MethodHandle method = lookup()
            .findStatic(TestMethodHandle1.class, "hello", type);
        method.<void>invoke();
    }
}

执行这个测试需要使用如下命令:
java -XX:+EnableMethodHandles TestMethodHandle1

(注意“java”要使用JDK 7的,不要用了JDK 6或更早的)

首先,要使用MethodHandle,需要引入的类型都在java.dyn包里。这个例子用到的是MethodHandles、MethodHandles.Lookup、MethodType、MethodHandle几个。
流程是:
0、调用MethodHandles.lookup()方法,遍历调用栈检查访问权限,然后得到一个MethodHandles.Lookup实例;该对象用于确认创建MethodHandle的实例的类对目标方法的访问权限是否满足要求,并提供搜索目标方法的逻辑;
1、指定目标方法的“方法类型”,得到一个MethodType实例;
2、通过MethodHandles.lookup()静态方法得到一个类型为MethodHandles.Lookup的工厂,然后靠它搜索指定的类型、指定的名字、指定的方法类型的方法,得到一个MethodHandle实例;
3、调用MethodHandle上的invoke方法。

其中,第1步中调用的MethodType.make()方法接收的参数是一组类型,第一个参数是返回类型,后面依次是各个参数的类型。上例中MethodType.make(void.class)得到的就是一个返回类型为void,参数列表为空的方法类型。如果熟悉Java字节码的话,这个方法类型的描述符就是()V。关于方法描述符的格式,可以参考JVM规范第二版4.3.3小节。MethodType的实例只代表所有返回值与参数类型匹配的一类方法的方法类型,自身没有名字;在检查某个方法是否与某个MethodType匹配时只考虑结构,可以算是一种特殊的structural-typing。

第2步看起来跟普通的反射很像,但通过反射得到的代表方法的对象是java.lang.reflect.Method的实例,它含有许多跟“执行”没有直接关系的信息,比较笨重;通过Method对象调用方法只是正常方法调用的模拟,所有参数会被包装为一个数组,开销较大。而MethodHandle则是个非常轻量的对象,主要目的就是用来引用方法并调用;通过它去调用方法不会导致参数被包装,原始类型的参数也不会被自动装箱。
MethodHandles.Lookup上有三个find方法,包括findStatic、findVirtual、findSpecial,分别对应invokestatic、invokevirtual/invokeinterface、invokespecial会对应的调用逻辑。注意到findVirtual方法所返回的MethodHandle的方法类型会包含一个显式的“this”参数作为第一个参数;调用这样的MethodHandle要显式传入“receiver”。这个看起来就跟.NET的开放委托相似,可以参考我之前的一帖。由于JDK 7的MethodHandle支持currying,可以把receiver保存在MethodHandle里,所以也可以创建出类似.NET的闭合委托的MethodHandle实例。
MethodHandles.Lookup上还有一组方法可以从通过反射API得到的Constructor、Field或Method对象创建出对应的MethodHandle。

第3步调用的MethodHandle.invoke()看似是一个虚方法,实际上并不是MethodHandle上真的存在的方法,而只是标记用的虚构出来的方法。上例中第13行对应的Java字节码是:
invokevirtual java/dyn/MethodHandle.invoke:()V

也就是假装MethodHandle上有一个描述符为()V且名为invoke的虚方法,通过invokevirtual指令去调用它。
Java编译器为它做特殊处理:返回值类型如同泛型参数在<>内指定,不写的话默认为返回Object类型;参数列表的类型则由Java编译器根据实际参数的表达式推断出来。与正常的泛型方法不同,MethodHandle.invoke指定返回值类型可以使用void和所有原始类型,不必像使用泛型方法时需要把原始类型写为对应的包装类型。
MethodHandle的方法类型不是Java语言的静态类型系统的一部分。虽然它的实例在运行时带有方法类型信息(MethodType),但在编译时Java编译器却不知道这一点。所以在编译时,调用invoke时传入任意个数、任意类型的参数都可以通过编译;但在运行时要成功调用,由Java编译器推断出来的返回值类型与参数列表必须与运行时MethodHandle实际的方法类型一致,否则会抛出WrongMethodTypeExceptionJohn Rose把MethodHandle.invoke的多态性称为“签名多态性(signature polymorphism)”。

用户可以自行继承java.dyn.JavaMethodHandle来创建自定义的MethodHandle子类,可以添加域或方法等,并可以指定该类型看作MethodHandle时的“入口点”——实际指向的方法。

许多JVM实现在JIT编译的时候会做激进的优化,包括常量传播、内联、逃逸分析、无用代码削除等许多。JDK 7的MethodHandle的一个好处是它就像它所指向的目标方法的替身一样,JVM原本可以做的优化对MethodHandle也一样支持,特别是有需要的时候可以把目标方法内联到调用处。相比之下,通过反射去调用方法则无法被JVM有效的优化。

对比C#的例子:
C# 2.0:
using System;

static class TestDelegate1 {
    static void Hello() {
        Console.WriteLine("Hello world!");
    }
    
    static void Main(string[] args) {       
        Action method = Hello; // Action method = new Action(Hello);
        method();              // method.Invoke();
    }
}

这段代码与前面Java版的TestMethodHandle1功能基本相同。来观察一下两者的异同点。

要在C#里使用委托的流程是:
0、事先声明好合适的委托类型;
1、适用合适的委托类型,指定目标方法创建出委托的实例;
2、调用委托上的Invoke()方法。

.NET允许用户自定义委托类型,在C#里的语法是:
modifiers delegate return_type DelegateTypeName(argument_list);

该语法与方法的声明语法非常像,只是在返回类型之前多了个delegate关键字而已,比C中typedef函数指针类型的语法容易多了。上例用到的System.Action类型就是标准库里声明好的一个委托类型,其声明形如:
namespace System {
    public delegate void Action();
}

这样声明出来的委托类型相当于声明了一个Action类,继承System.MulticastDelegate,并且拥有一个返回值类型为void,参数列表为空的Invoke()方法。在C#里,用户无法像声明普通类型一样通过声明一个继承System.Delegate或System.MulticastDelegate的类来得到一个新的委托类型,而只能用上述语法来声明。不过从CLR的角度看,并没有限制用户不能自行继承上述两种类型来声明新的委托类型。
委托是.NET类型系统的一部分。两个委托类型即便表示的签名一致也会被认为是不同的类型,不能相互赋值/转换。这体现出了C#与.NET类型的nominal-typing性质。创建委托实例时则只考虑目标方法与委托类型在签名上是否吻合,而不考虑名字问题,这点又与JDK 7的MethodHandle相似。
委托上的Invoke方法的签名与委托声明的相吻合。在编译时,调用委托的Invoke()方法与调用一般的虚方法一样会被类型检查。
目前在C#里可以显式调用委托上的Invoke()方法,也可以直接把委托当成方法用括号调用。是显式调用Invoke()还是直接用括号调用委托,现在来说只是程序员的偏好问题而已。事实上在C# 2.0以前编译器会阻止程序员显式调用Invoke()方法。

用C#代码例子再稍微解释一下:
// 下面声明两个委托类型,它们的签名是一样的
// (int, int) -> int
delegate int BinaryIntOp1(int x, int y);
delegate int BinaryIntOp2(int i, int j);
// 形式参数的类型是重要的,名字不重要
// 上面两个委托上的Invoke()方法都形如:
// [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]
// public virtual int Invoke(int i, int j);
// 注意到Invoke()是一个虚方法,其返回值与参数类型都是确定的,
// 而其实现是由运行时直接提供的。

class Demo {
    // 定义一个签名为(int, int) -> int的方法
    public static int Add(int a, int b) {
        return a + b;
    }
    
    static void Main(string[] args) {
        // 可以,Add方法与BinaryIntOp1要求的签名匹配
        BinaryIntOp1 op1 = new BinaryIntOp1(Add);

        // 可以,Add方法与BinaryIntOp1要求的签名匹配
        // C# 2.0的隐式创建委托的新特性:等同于写成new BinaryIntOp2(Add);
        BinaryIntOp2 op2 = Add;

        // 不行,两者属于不同的委托类型,无法相互赋值/转换
        //BinaryIntOp2 op3 = op1;
        
        // 调用Invoke()方法会被编译器检查类型是否匹配
        int sum = op1.Invoke(4, 2);
        // 下面这样就会在编译时出错:
        //int sum2 = op2.Invoke(new object(), 0);
    }
}


Java版例子中,创建MethodHandle对象需要在代码里通过MethodHandles.Lookup工厂来查找目标;在C#里编译器已经帮忙找出了目标方法的token,写起来方便许多;C#编译器会根据方法名与委托的签名去寻找合适重载版本的方法,找出它的token,并用于创建委托实例。如果目标方法无法在编译时确定,使用System.Delegate.CreateDelegate(Type, MethodInfo)方法也可以依靠反射信息创建出委托。

CLI的几个主流实现,微软的CLR、Novell的Mono等的JIT编译器都比较静态,遇到虚方法和委托调用都不会内联。从这点说,.NET比JVM的技术复杂度要低一些。不过至少在.NET里使用委托不会带来多少额外开销,所以还是可以放心使用的。

第二组例子,给hello world添加参数:

JDK 7:
import java.dyn.*;
import static java.dyn.MethodHandles.*;

public class TestMethodHandle2 {
    private static void hello(String name) {
        System.out.printf("Hello, %s!\n", name);
    }
    
    public static void main(String[] args) {
        if (0 == args.length) args = new String[] { "Anonymous" };
        
        MethodType type = MethodType.make(void.class, String.class);
        MethodHandle method = lookup()
            .findStatic(TestMethodHandle2.class, "hello", type);
        method.<void>invoke(args[0]);
    }
}

编译,以下述命令运行
java -XX:+EnableMethodHandles TestMethodHandle2 test

输出结果为:
引用
Hello, test!

基本上跟第一组例子一样,只是让hello()多了个参数而已。留意一下创建MethodType实例的代码如何对应的改变。
第15行对应的Java字节码是:
invokevirtual java/dyn/MethodHandle.invoke:(Ljava/lang/String;)V

留意Java编译器是如何根据调用invoke时传入的参数的静态类型(编译时类型)来决定invoke的方法描述符。(Ljava/lang/String;)V的意思是返回值类型为void,参数列表有一个参数,类型为java.lang.String。
如果把代码稍微修改,使MethodHandle的方法类型与Java编译器推断的调用类型不相符的话:
import java.dyn.*;
import static java.dyn.MethodHandles.*;

public class TestMethodHandle2 {
    private static void hello(Object name) {
        System.out.printf("Hello, %s!\n", name);
    }
    
    public static void main(String[] args) {
        if (0 == args.length) args = new String[] { "Anonymous" };
        
        MethodType type = MethodType.make(void.class, Object.class);
        MethodHandle method = lookup()
            .findStatic(TestMethodHandle2.class, "hello", type);
        method.<void>invoke(args[0]);
    }
}

编译运行会看到:
引用
Exception in thread "main" java.dyn.WrongMethodTypeException: (Ljava/lang/Object;)V cannot be called as (Ljava/lang/String;)V
        at TestMethodHandle2.main(TestMethodHandle2.java:15)

这演示了Java编译器将invoke的方法类型推断为(Ljava/lang/String;)V,而被调用的MethodHandle实例实际的方法类型却是(Ljava/lang/Object;)V,JVM便认为这个调用不匹配并拒绝执行。关键点是:调用invoke时,参数表达式的静态类型(编译时类型)必须与MethodHandle的方法类型中对于位置的参数类型“准确一致”;虽然String类型的引用可以隐式转换为Object类型的,但不满足“准确一致”的要求。
要想让修改过的TestMethodHandle2再次正确运行,可以把第15行改为:method.<void>invoke((Object)args[0]);,也就是加个类型转换,使Java编译器推断出来的方法描述符为(Ljava/lang/Object;)V。或者也可以加一个适配器:
import java.dyn.*;
import static java.dyn.MethodHandles.*;

public class TestMethodHandle2 {
    private static void hello(Object name) {
        System.out.printf("Hello, %s!\n", name);
    }
    
    public static void main(String[] args) {
        if (0 == args.length) args = new String[] { "Anonymous" };
        
        MethodType type = MethodType.make(void.class, Object.class);
        MethodType adaptedType = MethodType.make(void.class, String.class);
        MethodHandle method = lookup()
            .findStatic(TestMethodHandle2.class, "hello", type);
        MethodHandle adaptedMethod = MethodHandles.convertArguments(
            method, adaptedType);
        adaptedMethod.<void>invoke(args[0]);
    }
}

这里演示了MethodHandle的可组装性:通过给实际调用目标装一个转换参数类型的适配器,方法调用就又可以成功了。

对比C#的例子:
C# 3.0:
using System;

static class TestDelegate2 {
    static void Hello(string name) {
        Console.WriteLine("Hello, {0}!", name);
    }
    
    static void Main(string[] args) {
        if (0 == args.Length) args = new [] { "Anonymous" };
        Action<string> method = Hello;
        method(args[0]);
    }
}


基本上跟第一组例子也是一样的。Action<T>是标准库里预先声明好的一个泛型委托类型,其声明形如:
namespace System {
    public delegate void Action<T>(T t);
}

为了对比,下面也把Hello()的参数类型改为object,
using System;

static class TestDelegate2 {
    static void Hello(object name) {
        Console.WriteLine("Hello, {0}!", name);
    }
    
    static void Main(string[] args) {
        if (0 == args.Length) args = new [] { "Anonymous" };
        Action<object> method = Hello;
        method(args[0]);
    }
}

编译和运行都没有任何问题。这里要演示的是Invoke()方法是有确定的签名的,与委托类型声明的相吻合。编译器不会擅自推断Invoke()的签名。

第三组例子,可指定排序条件的快速排序:
前面一直在用hello world作例子或许是无聊了点,下面弄点稍微长一些的。

JDK 7:
import java.dyn.*;
import java.util.*;
import static java.dyn.MethodHandles.*;
import static java.lang.Integer.parseInt;

public class TestMethodHandle3 {
    private static int compareStringsByIntegerValue(String num1, String num2) {
        return parseInt(num1) - parseInt(num2);
    }
    
    private static int compareStringsByLength(String str1, String str2) {
        return str1.length() - str2.length();
    }
    
    public static void sort(String[] array, MethodHandle comparer) {
        if (0 == array.length) return;
        sort(array, 0, array.length - 1, comparer);
    }
    
    private static void sort(
        String[] array,
        int left,
        int right,
        MethodHandle comparer) {
        
        if (left >= right) return;
        
        String pivot = array[right];
        int lo = left - 1;
        int hi = right;
        while (true) {
            while (comparer.<int>invoke(array[++lo], pivot) < 0) {
            }
            while (hi > left
                && comparer.<int>invoke(array[--hi], pivot) > 0) {
            }
            if (lo >= hi) break;
            swap(array, lo, hi);
        }
        swap(array, lo, right);
        sort(array, left, lo - 1, comparer);
        sort(array, lo + 1, right, comparer);
    }
    
    private static <E> void swap(E[] array, int i, int j) {
        E temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    public static void main(String[] args) {
        String[] array = new String[] {
            "25", "02", "250", "48", "0024", "42", "2"
        };
        
        MethodType type = MethodType.make(
            int.class,
            String.class, String.class);
        MethodHandle comparer;
        
        comparer = lookup().findStatic(
            TestMethodHandle3.class,
            "compareStringsByIntegerValue",
            type);
        sort(array, comparer);
        for (String s : array) System.out.println(s);
        
        System.out.println();
        
        comparer = lookup().findStatic(
            TestMethodHandle3.class,
            "compareStringsByLength",
            type);
        sort(array, comparer);
        for (String s : array) System.out.println(s);
    }
}

编译,运行,输出结果为:
引用
02
2
0024
25
42
48
250

2
25
42
48
02
250
0024


核心的sort()方法是个简单的快排实现。为了让用户能够指定排序条件,我让它接收MethodHandle为参数来提供判断逻辑。在没有MethodHandle之前,我可能会选择使用策略模式来达到剥离出部分算法的目的。JDK里的许多API也是这么做的,例如Arrays.sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c),其中的Comparator参数就是“策略对象”。现在就可以直接用一个MethodHandle来代替它了。
例子的整体结构跟前两组没有显著的不同,应该还是比较好理解的。
值得注意的是,我把swap()写为了泛型方法,方便以后再更多地方能复用。排序也应该是个通用算法,为何不把sort()也写为泛型呢?
如果把例子中sort()方法里出现的String全部直接替换为泛型的,变成:
    public static <E> void sort(E[] array, MethodHandle comparer) {
        if (0 == array.length) return;
        sort(array, 0, array.length - 1, comparer);
    }
    
    private static <E> void sort(
        E[] array,
        int left,
        int right,
        MethodHandle comparer) {
        
        if (left >= right) return;
        
        E pivot = array[right];
        int lo = left - 1;
        int hi = right;
        while (true) {
            while (comparer.<int>invoke(array[++lo], pivot) < 0) {
            }
            while (hi > left
                && comparer.<int>invoke(array[--hi], pivot) > 0) {
            }
            if (lo >= hi) break;
            swap(array, lo, hi);
        }
        swap(array, lo, right);
        sort(array, left, lo - 1, comparer);
        sort(array, lo + 1, right, comparer);
    }

编译没问题,但运行的时候会看到comparer.<int>invoke(...)的调用抛WrongMethodTypeException异常。你可能会纳闷:array数组的元素类型不是E么,是泛型的啊;以String为泛型参数调用sort()的时候,invoke的实际参数类型与comparer的方法类型应该匹配才对,怎么会出错呢?
问题就在于Java的泛型是通过类型擦除法(type-erasure)来实现的,编译过后所有泛型参数都变为了Object,这里也不例外。所以Java编译器推断出来的invoke()的描述符是(Ljava/lang/Object;Ljava/lang/Object;)I,这就出现我们在第二组例子里遇到的问题了——方法类型不准确匹配。
于是解决的办法也很简单,只要改成这样:
    public static <E> void sort(E[] array, MethodHandle comparer) {
        if (0 == array.length) return;
        Class<Object> objClz = Object.class;
        MethodType adaptedType = MethodType.make(int.class, objClz, objClz);
        MethodHandle adaptedComparer = MethodHandles.convertArguments(
            comparer, adaptedType);
        sort(array, 0, array.length - 1, adaptedComparer);
    }

在调用核心的sort()方法前,先加一个适配器来解决类型差异的问题,就万事大吉了 ^ ^

对比C#的例子:
C# 2.0
using System;
using System.Collections.Generic;

static class TestDelegate3 {
    static int CompareStringsByIntegerValue(string num1, string num2) {
        return Convert.ToInt32(num1) - Convert.ToInt32(num2);
    }
    
    static int CompareStringsByLength(string str1, string str2) {
        return str1.Length - str2.Length;
    }
    
    public static void Sort<T>(T[] array, Comparison<T> comparer) {
        Sort(array, 0, array.Length - 1, comparer);
    }
    
    static void Sort<T>(
        T[] array,
        int left,
        int right,
        Comparison<T> comparer) {
        
        if (left >= right) return;
        
        T pivot = array[right];
        int lo = left - 1;
        int hi = right;
        while (true) {
            while (comparer(array[++lo], pivot) < 0) {
            }
            while (hi > left
                && comparer(array[--hi], pivot) > 0) {
            }
            if (lo >= hi) break;
            Swap(ref array[lo], ref array[hi]);
        }
        Swap(ref array[lo], ref array[right]);
        Sort(array, left, lo - 1, comparer);
        Sort(array, lo + 1, right, comparer);
    }
    
    static void Swap<T>(ref T t1, ref T t2) {
        T temp = t1;
        t1 = t2;
        t2 = temp;
    }
    
    static void Main(string[] args) {
        String[] array = new String[] { "25", "02", "250", "48", "0024", "42", "2" };
        
        Sort(array, CompareStringsByIntegerValue);
        foreach (var s in array) Console.WriteLine(s);
        
        Console.WriteLine();
        
        Sort(array, CompareStringsByLength);
        foreach (var s in array) Console.WriteLine(s);
    }
}


基本上也没什么需要特别解释的了。System.Comparison<T>是标准库里预先声明的一个泛型委托类型,其声明形如:
namespace System {
    public delegate int Comparison<T>(T x, T y);
}

注意.NET里委托与泛型可以很好的结合在一起使用,而不必考虑转换参数类型的问题,因为.NET的泛型信息会带到运行时。或者说,“.NET generics are reified”。

如果用上C# 3.0和.NET Framework 3.5的新特性,使用lambda表达式来提供排序依据,
using System;
using System.Collections.Generic;

static class TestDelegate3 {
    public static void Sort<T>(T[] array, Func<T, T, int> comparer) {
        Sort(array, 0, array.Length - 1, comparer);
    }
    
    static void Sort<T>(
        T[] array,
        int left,
        int right,
        Func<T, T, int> comparer) {
        
        if (left >= right) return;
        
        T pivot = array[right];
        int lo = left - 1;
        int hi = right;
        while (true) {
            while (comparer(array[++lo], pivot) < 0) {
            }
            while (hi > left
                && comparer(array[--hi], pivot) > 0) {
            }
            if (lo >= hi) break;
            Swap(ref array[lo], ref array[hi]);
        }
        Swap(ref array[lo], ref array[right]);
        Sort(array, left, lo - 1, comparer);
        Sort(array, lo + 1, right, comparer);
    }
    
    static void Swap<T>(ref T t1, ref T t2) {
        T temp = t1;
        t1 = t2;
        t2 = temp;
    }
    
    static void Main(string[] args) {
        var array = new [] { "25", "02", "250", "48", "0024", "42", "2" };
        
        Sort(array,
            (s1, s2) => Convert.ToInt32(s1) - Convert.ToInt32(s2));
        foreach (var s in array) Console.WriteLine(s);
        
        Console.WriteLine();
        
        Sort(array,
            (s1, s2) => s1.Length - s2.Length);
        foreach (var s in array) Console.WriteLine(s);
    }
}

我们就不必再为例中用到的排序条件写具名方法了。实际上C#编译器仍然为这两个lambda表达式生成了私有静态方法,我们得到的东西是一样的(硬要说的话,少得到了俩名字),但需要写在代码里的东西减少了。
如果进一步改为使用标准库中现成的方法来排序,
using System;
using System.Collections.Generic;

static class TestDelegate3 {    
    static void Main(string[] args) {
        var array = new [] { "25", "02", "250", "48", "0024", "42", "2" };
        
        Array.Sort(array,
            (s1, s2) => Convert.ToInt32(s1) - Convert.ToInt32(s2));
        foreach (var s in array) Console.WriteLine(s);
        
        Console.WriteLine();
        
        Array.Sort(array,
            (s1, s2) => s1.Length - s2.Length);
        foreach (var s in array) Console.WriteLine(s);
    }
}

整个代码就简洁了许多。


Alright,这帖就介绍MethodHandle与.NET的委托到这里。希望上面的例子达到了简单介绍JDK 7的MethodHandle的目的。
你可能会心存疑惑,“我为什么需要引用别的方法呢?” 由于Java以前一直没有提供这样的功能,一直只使用Java的人可能较少思考这个问题。但你可能会碰到过这样的问题,有时候要调用的目标只有到了运行的时候才知道,无法在代码里直接写方法调用,那怎么办?以前的解决办法就只有通过反射了。一个最简单的使用场景就是,原本通过反射做的方法调用,现在都可以通过MethodHandle完成。如果一个方法频繁被反射调用,开销会很明显,而且难以优化;通过MethodHandle调用则跟正常的接口方法调用速度接近,没有各种包装/装箱的开销,而且可以被JVM优化,何乐而不为?
在MethodHandle出现之前,许多JVM上的脚本语言实现,如JRuby,为了提高调用方法的速度,选择生成大量很小的“invoker类”来对方法签名做特化,通过它们去调用目标方法,避免反射调用的开销。但这么做有许多问题,一是实现麻烦,而是对PermGen堆带来巨大的压力。MethodHandle的出现极大的改善了状况,既便于使用,效率又高,而且还不会对PermGen带来多少压力。Charles Nutter在一帖中描述了这个问题
编辑:经指正,Groovy当前还没有用invoke stub,只是做了callsite caching优化。我只读过Groovy的编译器的代码,没读过运行时部分的代码,忽悠了同学们了,抱歉 <(_ _)>

进一步介绍等以后写invokedynamic的时候再写了~ until then

Have fun ^ ^
分享到:
评论
32 楼 srdrm 2009-09-29  
看这情况应该引入新的关键字
31 楼 RednaxelaFX 2009-09-28  
starfeng 写道
引用
定位到一个java方法,其实只需要类型(Class),方法名及参数即可。
...

MethodHandle版:
private static void test(java.dyn.MethodHandle);
    Signature: (Ljava/dyn/MethodHandle;)V
    flags: ACC_PRIVATE, ACC_STATIC    LineNumberTable:
      line 7: 0
      line 8: 8
      line 7: 15
      line 10: 21
    Code:
      stack=4, locals=2, args_size=1
         0: iconst_0     
         1: istore_1     
         2: iload_1      
         3: ldc           #2                  // int 100000
         5: if_icmpge     21
         8: aload_0      
         9: iconst_1     
        10: iconst_2     
        11: iconst_3     
        12: invokevirtual #3                  // Method java/dyn/MethodHandle.invoke:(III)V
...
普通反射版:
private static void test(java.lang.reflect.Method) throws java.lang.Throwable;
    Signature: (Ljava/lang/reflect/Method;)V
    flags: ACC_PRIVATE, ACC_STATIC    LineNumberTable:
      line 7: 0
      line 8: 8
      line 7: 39
      line 10: 45
    Code:
      stack=6, locals=2, args_size=1
         0: iconst_0     
         1: istore_1     
         2: iload_1      
         3: ldc           #2                  // int 100000
         5: if_icmpge     45
         8: aload_0      
         9: aconst_null  
        10: iconst_3     
        11: anewarray     #3                  // class java/lang/Object
        14: dup          
        15: iconst_0     
        16: iconst_1     
        17: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        20: aastore      
        21: dup          
        22: iconst_1     
        23: iconst_2     
        24: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        27: aastore      
        28: dup          
        29: iconst_2     
        30: iconst_3     
        31: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        34: aastore      
        35: invokevirtual #5                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
...


写得很多,看得也累。。。但感觉整个就是答错了方向.
再加上后面回复的人,有好几个在不懂装懂的,整个就一误人子弟的贴。

MethodHandle我还没来得及深究,但汇编,java字节码还是有不少了解的。
如果你认为性能差距的原因是来自:
引用
17: invokestatic  #4                  // Method java/lang/Integer.valueOf:

这类代码,那么,麻烦你改一改测试类,统一成
 private static void doNothing(Integer x, Integer y, Integer z) { } 

再比较字节码。
另外,
引用
35: invokevirtual #5                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;

这个代码和
引用
12: invokevirtual #3                  // Method java/dyn/MethodHandle.invoke:(III)V

相比,就调用本身来说,是没有差别的,唯一差别是在的两个方法的native c的实现上。如果你分析这段c的代码再得出性能差别的原因,才是找对了方向。

另外,如果我没搞错的话,MethodHandle引入返回值查找方法,其根本原因是来自动态语言的返回值与具体类型无关,和性能本身没什么关系。然后,
引用
如果只是要做Java的method overload resolution,当然只要参数类型不要返回值类型就够了,但了解class文件及JVM内部数据组织方式的话就会知道,方法的签名(signature)在class文件里是以方法描述符(method descriptor)的形式存在,而该描述符上是有返回值类型的。MethodHandles的API这么设计就是为了快,能更直接的访问VM里的信息,以最快的方式找到目标方法。

这个只是你自已的推论吧。这个就如同这么一个sql的比方:原先是查询是id=1, 你说,不够快,id=1&retType=2才更快。

如果MethodHandles.Lookup跟普通反射一样,只通过参数类型去搜索目标方法,那么它与JVM内部所持有的信息就有不匹配的地方,而需要去考虑可能遇到多个匹配结果的问题,然后产生异常发出来,速度自然就慢了。当前的API设计要求指定返回值类型,与JVM持有的信息一致,肯定能找到唯一的方法(或者是方法不存在),速度为何不应该更好?

我之所以选用接收int为参数的类型作为目标方法自然是有目的的,而那就是找一个反射调用是开销非常大的状况,拿来与MethodHandle对比。这样容易显示出差异。事实上一开始我写那两组测试的时候的doNothing就是无参的空方法,后来觉得效果要更明显些比较有趣,就改为接收多个int的。

starfeng 写道
再比较字节码。
另外,
引用
35: invokevirtual #5                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;


这个代码和
引用
12: invokevirtual #3                  // Method java/dyn/MethodHandle.invoke:(III)V

相比,就调用本身来说,是没有差别的,唯一差别是在内部的native c的实现上。

单就“invokevirtual”指令来说,从字节码上是看不到区别。然而速度的差异来自两部分,
1、Method.invoke在Java代码的一侧有固有的额外开销,包括可变长度参数数组包装以及原始类型参数的自动装箱。即便目标方法接收的参数是引用类型的,没有原始类型参数的装箱开销,数组包装是无法避免的。即使调用method.invoke(null)也会导致一个空Object[]数组的创建。这个并不是单在那条“invokevirtual”上的开销。
starfeng 写道
那么,麻烦你改一改测试类,统一成
//...

这句话请您自己先试了,把结果贴出来,看看数组包装的代码是否仍然存在,而反射的开销是否仍然比MethodHandle大。

2、Method.invoke在VM内的一侧有固有的额外开销,并且会阻碍JIT编译器的优化;而MethodHandle的实现是它比普通反射的固有额外开销少,而且不会阻碍编译器的优化(当前实现尚未完善)。无论是通过MethodHandle.invoke还是通过Method.invoke,我们要达到的目的都是调用“实际目标方法”,而不想去关心这些invoke的实现;也就是说,如果这些invoke本身很复杂,对我们来说就是不利的。同样使用invokevirtual指令去调用“invoke”方法是事实,但跟invoke到实际目标方法的距离没有关系,而这“距离”正是两种方式的性能差异的第二个大头。

你该不会认为“native c”实现的东西的运行代价都小到可以忽略不计吧。另外HotSpot VM是用C++与内联汇编而不是C来实现的,准确说也不是“native c”。
我前面已经提到过Method.invoke会涉及的一些内部实现的方法,用它们做关键字相信你能搜到满意的信息。
简单的说,反射调用的过程中涉及多次安全检查,涉及查询类型信息去找到方法的实际指针,涉及包装类型到原始类型的拆箱,最后还有涉及参数数组解包装,通过stub把参数合适的放到栈上,最后才跑到实际目标方法里;如果其中一些安全检查的结果是可以被cache住的,但另外一些流程则每次调用都要经历,所以速度才慢。
相比之下,MethodHandle.invoke则直接的多。前面例子里用的大多是DirectMethodHandle,它直接指向目标方法,所有安全检查都是在创建MethodHandle的实例是就已经完成,调用MethodHandle.invoke完全不涉及再做安全检查的开销。它不涉及对目标方法的搜索,因为它持有的token信息让VM直接知道目标方法是哪一个。而且由于参数没有被数组包装,它不用经过stub去调整参数而可以直接把参数传递给实际的目标方法。总体来说,在没有被内联的前提下,它跟接口方法调用涉及的一些过程非常相似。现在它速度没有接口方法调用快只是因为JDK 7还在开发过程中,一些既定的实现目标还没达到而已。

嘛……欢迎争论~ =v=
不过请提出确实的依据。如果你调试过,读过相关源码,仍然得到你现在的结论,那我很高兴继续与你争论。
30 楼 iaimstar 2009-09-28  
又有看的了...high呀
29 楼 starfeng 2009-09-28  
引用
定位到一个java方法,其实只需要类型(Class),方法名及参数即可。
...

MethodHandle版:
private static void test(java.dyn.MethodHandle);
    Signature: (Ljava/dyn/MethodHandle;)V
    flags: ACC_PRIVATE, ACC_STATIC    LineNumberTable:
      line 7: 0
      line 8: 8
      line 7: 15
      line 10: 21
    Code:
      stack=4, locals=2, args_size=1
         0: iconst_0     
         1: istore_1     
         2: iload_1      
         3: ldc           #2                  // int 100000
         5: if_icmpge     21
         8: aload_0      
         9: iconst_1     
        10: iconst_2     
        11: iconst_3     
        12: invokevirtual #3                  // Method java/dyn/MethodHandle.invoke:(III)V
...
普通反射版:
private static void test(java.lang.reflect.Method) throws java.lang.Throwable;
    Signature: (Ljava/lang/reflect/Method;)V
    flags: ACC_PRIVATE, ACC_STATIC    LineNumberTable:
      line 7: 0
      line 8: 8
      line 7: 39
      line 10: 45
    Code:
      stack=6, locals=2, args_size=1
         0: iconst_0     
         1: istore_1     
         2: iload_1      
         3: ldc           #2                  // int 100000
         5: if_icmpge     45
         8: aload_0      
         9: aconst_null  
        10: iconst_3     
        11: anewarray     #3                  // class java/lang/Object
        14: dup          
        15: iconst_0     
        16: iconst_1     
        17: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        20: aastore      
        21: dup          
        22: iconst_1     
        23: iconst_2     
        24: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        27: aastore      
        28: dup          
        29: iconst_2     
        30: iconst_3     
        31: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        34: aastore      
        35: invokevirtual #5                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
...


写得很多,看得也累。。。但感觉整个就是答错了方向.
再加上后面回复的人,有好几个在不懂装懂的,整个就一误人子弟的贴。

MethodHandle我还没来得及深究,但汇编,java字节码还是有不少了解的。
如果你认为性能差距的原因是来自:
引用
17: invokestatic  #4                  // Method java/lang/Integer.valueOf:

这类代码,那么,麻烦你改一改测试类,统一成
 private static void doNothing(Integer x, Integer y, Integer z) { } 

再比较字节码。
另外,
引用
35: invokevirtual #5                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;

这个代码和
引用
12: invokevirtual #3                  // Method java/dyn/MethodHandle.invoke:(III)V

相比,就调用本身来说,是没有差别的,唯一差别是在的两个方法的native c的实现上。如果你分析这段c的代码再得出性能差别的原因,才是找对了方向。

另外,如果我没搞错的话,MethodHandle引入返回值查找方法,其根本原因是来自动态语言的返回值与具体类型无关,和性能本身没什么关系。然后,
引用
如果只是要做Java的method overload resolution,当然只要参数类型不要返回值类型就够了,但了解class文件及JVM内部数据组织方式的话就会知道,方法的签名(signature)在class文件里是以方法描述符(method descriptor)的形式存在,而该描述符上是有返回值类型的。MethodHandles的API这么设计就是为了快,能更直接的访问VM里的信息,以最快的方式找到目标方法。

这个只是你自已的推论吧。这个就如同这么一个sql的比方:原先是查询是id=1, 你说,不够快,id=1&retType=2才更快。
28 楼 RednaxelaFX 2009-09-28  
star022 写道
Java代码
findStatic(  
    TestMethodHandle1.class, // 方法所属类型(Class)  
    "hello",                 // 方法名  
    type                     // 由参数和返回值类型组成的“方法类型”  
); 

type// 由参数和返回值类型组成的“方法类型”
“方法类型”这个参数设计得很失败,
一个类中的方法,如果方法名和参数个数及类型一样,这个类能正确编译吗?
其实MethodHandle最终只需要暴露类似这样一个静态方法即可:
//方法如果没有返回值,为void
MethodHandle.<T>invoke(
    Class clazz, // 方法所属类型(Class)  
     String methodName, // 方法名  
     Object... params//方法参数,可以运行时确定参数个数及类型,定位到具体方法
)


你的思路被局限在Java“语言”里了。JSR 292的主要服务对象是JVM上的动态语言,而不是Java。如果你了解JVM的spec而不只是Java的spec,你应该能理解Java字节码不是只能通过Java编译器来生成的。你可以把我之前回帖的那段完整引用一次:
RednaxelaFX 写道
star022 写道
定位到一个java方法,其实只需要类型(Class),方法名及参数即可。

对,说得一点也没错,所以MethodHandles的API就是这样的:
引用
findStatic(
    TestMethodHandle1.class, // 方法所属类型(Class)
    "hello",                 // 方法名
    type                     // 由参数和返回值类型组成的“方法类型”
);

如果只是要做Java的method overload resolution,当然只要参数类型不要返回值类型就够了,但了解class文件及JVM内部数据组织方式的话就会知道,方法的签名(signature)在class文件里是以方法描述符(method descriptor)的形式存在,而该描述符上是有返回值类型的。MethodHandles的API这么设计就是为了快,能更直接的访问VM里的信息,以最快的方式找到目标方法。


如果那段文字仍然不能让你明白,那请看下面的例子。
首先要明确的是,在Java语言里,method overload只依赖于方法名和参数类型,不考虑返回值类型;仅在返回值类型不同的方法无法通过Java编译器的编译。
但生成Java字节码的方式有很多:JVM上有非常多其它语言,它们的编译器都可以生成Java字节码;动态代理要生成字节码;再不行,手工生成字节码也是可以的。从JVM的角度看,无论字节码的来源是什么,只要符合class文件规范、只要加载成功,JVM就可以执行那些字节码。
这里我用bitescript来生成一个class文件,类名为TestMethodSameName,包括两个foo方法,它们只在返回值类型上不同:
require 'rubygems'
require 'bitescript'
include BiteScript

fb = FileBuilder.build(__FILE__) do
  public_class 'TestMethodSameName' do
    public_static_method 'foo', void, int do
      ldc 'TestMethodSameName.foo:(I)V'
      aprintln
      returnvoid
    end
    
    public_static_method 'foo', int, int do
      ldc 'TestMethodSameName.foo:(I)I'
      aprintln
      iload 0
      ireturn
    end
    
    public_static_method 'main', void, string[] do
      push_int 123
      invokestatic this, 'foo', [void, int]
      push_int 456
      invokestatic this, 'foo', [int, int]
      pop
      returnvoid
    end
  end
end

fb.generate do |filename, class_builder|
  File.open(filename, 'w') do |file|
    file.write(class_builder.generate)
  end
end

得到的class文件,内容如下:
Compiled from "test5.rb"
public class TestMethodSameName extends java.lang.Object{
public static void foo(int);
  Code:
   0:   ldc     #9; //String TestMethodSameName.foo:(I)V
   2:   getstatic       #15; //Field java/lang/System.out:Ljava/io/PrintStream;
   5:   swap
   6:   invokevirtual   #21; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
   9:   return

public static int foo(int);
  Code:
   0:   ldc     #24; //String TestMethodSameName.foo:(I)I
   2:   getstatic       #15; //Field java/lang/System.out:Ljava/io/PrintStream;
   5:   swap
   6:   invokevirtual   #21; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
   9:   iload_0
   10:  ireturn

public static void main(java.lang.String[]);
  Code:
   0:   bipush  123
   2:   invokestatic    #28; //Method foo:(I)V
   5:   sipush  456
   8:   invokestatic    #30; //Method foo:(I)I
   11:  pop
   12:  return

}

如果用Java语法来写,就是:
public class TestMethodSameName {
    public static void foo(int i) {
        System.out.println("TestMethodSameName.foo:(I)V");
    }

    public static int foo(int i) {
        System.out.println("TestMethodSameName.foo:(I)I");
        return i;
    }

    public static void main(String[] args) {
        foo(123); // foo:(I)V
        foo(456); // foo:(I)I
    }
}

再次注意到这段代码用Java编译器确实编译不了。但上面生成的字节码对JVM来说却是完全没问题的。执行结果输出如下:
引用
TestMethodSameName.foo:(I)V
TestMethodSameName.foo:(I)I

这很好的说明了在深入到底层去挖掘MethodHandle时,指定返回值类型的必要性。
JDK原本包含的普通反射API之所以不需要指定返回值类型是因为它只是为Java语言服务的。如今的JSR 292则是为JVM上所有语言服务,主要目标是各种动态语言,但也不拒绝Java去使用它。

关于bitescript的用法,请参考这一帖的例子。上面生成的class文件我也放在附件里了,不相信例子的输出结果的话请自己执行一下,眼见为实。
27 楼 star022 2009-09-28  
Java代码
findStatic(  
    TestMethodHandle1.class, // 方法所属类型(Class)  
    "hello",                 // 方法名  
    type                     // 由参数和返回值类型组成的“方法类型”  
); 

type// 由参数和返回值类型组成的“方法类型”
“方法类型”这个参数设计得很失败,
一个类中的方法,如果方法名和参数个数及类型一样,这个类能正确编译吗?
其实MethodHandle最终只需要暴露类似这样一个静态方法即可:
//方法如果没有返回值,为void
MethodHandle.<T>invoke(
    Class clazz, // 方法所属类型(Class)  
     String methodName, // 方法名  
     Object... params//方法参数,可以运行时确定参数个数及类型,定位到具体方法
)

26 楼 RednaxelaFX 2009-09-28  
unsid 写道
没有jsr292的时候,用beanshell不是可以很好的模拟对于动态语言的支持么?

BeanShell是JVM上的“一种”动态脚本语言。JRuby、Jython、Rhino这些语言的实现不太可能依靠BeanShell的动态能力去解决。JSR 292就是为了让这些语言更容易实现,实现出来更高效而设计的。我在顶楼的帖的最后留了篇Charles Nutter讲在invokedynamic出现之前实现JRuby的一些麻烦的地方,可以读一下。需要翻墙,这个请自行解决~
25 楼 aws 2009-09-28  
还是函数指针好
什么时候java把指针也放出来得了
24 楼 unsid 2009-09-28  
没有jsr292的时候,用beanshell不是可以很好的模拟对于动态语言的支持么?
23 楼 RednaxelaFX 2009-09-27  
JohnnyJian 写道
反射和MethodHandle差10倍,那MethodHandle和直接的调用差多少?

果然要问这个问题么……嘛,测一下也不是不行。
不过仍然需要强调的是,JDK 7里的MethodHandle的内部设计与API设计都还没定案,还在不断改进中。HotSpot目前对MethodHandle.invoke的内联支持也还不彻底,所以拿现在的MethodHandle跟直接调用来比较会有明显的差距。即便如此它已经比普通的反射调用要快很多。最终的目标是让MethodHandle.invoke跟接口方法调用的速度差不多。

那么废话少说,上代码:
直接调用静态方法:
public class SpeedTrap {
    private static void doNothing(int x, int y, int z) { }
    
    private static void test() {
        for (int i = 0; i < 100000; i++) {
            doNothing(1, 2, 3);
        }
    }
    
    public static void main(String[] args) {
        // warm up
        for (int i = 0; i < 10; i++) {
            test();
        }
        
        // time the test
        long start = System.nanoTime();
        test();
        long end = System.nanoTime();
        System.out.println("elapse time: " + (end - start));
    }
}


直接调用接口方法:
interface Callable3 {
    void call(int x, int y, int z);
}

class Callable3Impl implements Callable3 {
    public void call(int x, int y, int z) { }
}

public class SpeedTrap3 {    
    private static void test(Callable3 c) {
        for (int i = 0; i < 100000; i++) {
            c.call(1, 2, 3);
        }
    }
    
    public static void main(String[] args) {
        Callable3 c = new Callable3Impl();

        // warm up
        for (int i = 0; i < 10; i++) {
            test(c);
        }
        
        // time the test
        long start = System.nanoTime();
        test(c);
        long end = System.nanoTime();
        System.out.println("elapse time: " + (end - start));
    }
}


前面的我给出的MethodHandle与普通反射的比较,用的例子是针对静态方法为目标的调用。实际上直接调用静态方法算是HotSpot里最容易优化的一种调用了,所以测试耗时很短:
引用
elapse time: 134933
elapse time: 134933
elapse time: 134934
elapse time: 134934
elapse time: 135213

相比之下,接口方法调用就慢一些,
引用
elapse time: 469054
elapse time: 468495
elapse time: 475759
elapse time: 468496
elapse time: 468775

MethodHandle.invoke最后就应该能达到接近这个水平。

为什么这两组测试比前面两组测试快那么多呢?因为我们要测试的“对象”——方法调用消失了。继续看代码,
静态方法调用版的test方法:
  ;; 函数入口处理(prologue)
  0x00be6890: mov    %eax,-0x4000(%esp)
  0x00be6897: push   %ebp
  0x00be6898: mov    %esp,%ebp
  0x00be689a: sub    $0x18,%esp         ;*iconst_0
                                        ; - SpeedTrap::test@0 (line 5)
  ;; 函数体开始
  ;; 循环初始化
  0x00be689d: mov    $0x0,%esi
  0x00be68a2: jmp    0x00be68af         ;*istore_0
                                        ; - SpeedTrap::test@1 (line 5)
  0x00be68a7: nop    

  ;; 循环体开始
  ;; doNothing()方法的调用被内联进来而消失了
  0x00be68a8: inc    %esi               ; OopMap{off=25}
                                        ;*goto
                                        ; - SpeedTrap::test@17 (line 5)
  0x00be68a9: test   %eax,0x990100      ;*goto
                                        ; - SpeedTrap::test@17 (line 5)
                                        ;   {poll}
  ;; 循环条件
  0x00be68af: cmp    $0x186a0,%esi
  0x00be68b5: jl     0x00be68a8         ;*if_icmpge
                                        ; - SpeedTrap::test@5 (line 5)
  ;; 函数出口处理(epilogue)
  0x00be68b7: mov    %ebp,%esp
  0x00be68b9: pop    %ebp
  0x00be68ba: test   %eax,0x990100      ;   {poll_return}
  0x00be68c0: ret

接口方法调用版的test方法:
  ;; 函数入口处理(prologue)
  0x00be7230: mov    %eax,-0x4000(%esp)
  0x00be7237: push   %ebp
  0x00be7238: mov    %esp,%ebp
  0x00be723a: sub    $0x18,%esp         ;*iconst_0
                                        ; - SpeedTrap3::test@0 (line 11)
  ;; 函数体开始
  ;; 循环初始化
  0x00be723d: mov    $0x0,%esi
  0x00be7242: jmp    0x00be726c         ;*istore_1
                                        ; - SpeedTrap3::test@1 (line 11)
  0x00be7247: nop    

  ;; 循环体开始
  0x00be7248: cmp    $0x0,%ecx          ; 空指针检查(检查参数c是否为空)
  0x00be724b: je     0x00be7261         ; 空指针时跳转到0x00be7261
  0x00be7251: mov    0x4(%ecx),%ebx     ; 这条与下条指令检查c的类型是否为Callable3Impl
  0x00be7254: cmpl   $0x14230e10,0x20(%ebx)  ;   {oop('Callable3Impl')}
  0x00be725b: jne    0x00be727e         ; c不是类型的实例则跳转到0x00be727e
  0x00be7261: mov    %ecx,%edi
  0x00be7263: cmp    (%ecx),%eax        ;*invokeinterface call
                                        ; - SpeedTrap3::test@12 (line 12)
                                        ; implicit exception: dispatches to 0x00be7294
  ;; 实际的c.call()的调用被内联进来而消失
  0x00be7265: inc    %esi               ; OopMap{ecx=Oop off=54}
                                        ;*goto
                                        ; - SpeedTrap3::test@20 (line 11)
  0x00be7266: test   %eax,0x990100      ;*goto
                                        ; - SpeedTrap3::test@20 (line 11)
                                        ;   {poll}
  ;; 循环条件
  0x00be726c: cmp    $0x186a0,%esi
  0x00be7272: jl     0x00be7248         ;*if_icmpge
                                        ; - SpeedTrap3::test@5 (line 11)
  ;; 函数出口处理(epilogue)
  0x00be7274: mov    %ebp,%esp
  0x00be7276: pop    %ebp
  0x00be7277: test   %eax,0x990100      ;   {poll_return}
  0x00be727d: ret

这次就不写那么详细的注释了,相信参考之前的代码也可以理解个大概。
关键点就是:原本应该有call指令进行方法调用的地方,现在消失了。这就是方法内联的效果。因为被内联的是空方法,内联进来之后自然是什么也不留下了。
由于静态方法不参与继承/重写相关的多态,可以说是“编译时确定的目标”,所以静态方法是最容易内联的,不需要做额外的检查。
而虚方法/接口方法则实际调用的版本取决于receiver的类型,要内联的话就必须要做一定检查:
·如果只记录前一次调用遇到的receiver类型(或其它影响dispatch的信息),这种callsite cache就叫做monomorphic inline cache,简称MIC;
·如果记录之前多次调用遇到的receiver类型(或其它影响dispatch的信息),这种callsite cache就叫做polymorphic inline cache,简称PIC。
还有所谓megamorphic状态,一般是指receiver变化太多,不值得做inline caching,而总是采取较慢的传统方式搜索目标方法。
上面的接口方法调用测试中展现的就是MIC:先检查receiver类型是否为某个已知类型(Callable3Impl),如果是的话就直接执行内联版本的c.call();否则退回到搜索方法的逻辑,并视情况决定是否更新或取消MIC。

正是因为MethodHandle.invoke在目前的JDK 7中尚未彻底实现inline功能,所以其开销比接口方法调用还是大很多。不过有两个工程师已经在努力实现相关功能了,可以期待以后的性能改善。
22 楼 JohnnyJian 2009-09-27  
反射和MethodHandle差10倍,那MethodHandle和直接的调用差多少?
21 楼 treblesoftware 2009-09-27  
Saito 写道
treblesoftware 写道
怎么 怎么 怎么 ,第一个例子怎么越看越像反射。

童鞋是不是再仔细看看整篇文章? .. 

    btw: 老赵也就是看见Twitter来一下. 不看见Twitter就窝在blogcn码字..多无趣啊..


其实,我只是调解一下紧张的气氛,才这么说的。
20 楼 star022 2009-09-27  
定位到一个java方法,其实只需要类型(Class),方法名及参数即可。
19 楼 star022 2009-09-27  
dennis_zane 写道
JeffreyZhao 写道
C#中的委托非常实用,但Java的这个做法有多少可用性啊


文章中说了,作为重量级的method reflection的替代品还是不错的。比之策略模式来说,这个method handler的调用方式还是比较恶心。



java的反射现在还重吗?
试试反射调用10w次花多少时间?

这个method handler的使用方式上的确没多少创意,显得还是不够简洁,跟自己写反射工具类使用上区别不大
18 楼 kunee 2009-09-27  
委托 不错
17 楼 RednaxelaFX 2009-09-27  
JohnnyJian 写道
当前版本的Groovy只做了callsite优化,而并没有生成大量的“invoker类”

呃……没忽悠过去,改改~多谢 ^ ^
另外Charles去年也对比过当时的JRuby与Groovy采用的优化技巧,包括method handles和call site caching。有兴趣的同学可以看看~
16 楼 JohnnyJian 2009-09-27  
当前版本的Groovy只做了callsite优化,而并没有生成大量的“invoker类”
15 楼 Saito 2009-09-26  
treblesoftware 写道
怎么 怎么 怎么 ,第一个例子怎么越看越像反射。

童鞋是不是再仔细看看整篇文章? .. 

    btw: 老赵也就是看见Twitter来一下. 不看见Twitter就窝在blogcn码字..多无趣啊..
14 楼 treblesoftware 2009-09-26  
怎么 怎么 怎么 ,第一个例子怎么越看越像反射。
13 楼 RednaxelaFX 2009-09-26  
MethodHandle跟动态代理要解决的不是同一个问题
我相信即便JDK 7整合进了MethodHandle,如果你不用JVM上别的一些语言,完全不*需要*MethodHandle去解决你手头的问题。
Hmm……让我想像一下用动态代理去实现策略模式是什么样的

相关推荐

Global site tag (gtag.js) - Google Analytics