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

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

    博客分类:
  • Java
阅读更多
原帖在http://www.iteye.com/topic/477934?page=3#1185374,顺手转进来

star022 写道
定位到一个java方法,其实只需要类型(Class),方法名及参数即可。

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

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

star022 写道
dennis_zane 写道
JeffreyZhao 写道
C#中的委托非常实用,但Java的这个做法有多少可用性啊


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



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

这个method handler的使用方式上的确没多少创意,显得还是不够简洁,跟自己写反射工具类使用上区别不大

API上看起来区别不大那就对了,这样API就能很快上手。关键的差异都在VM内部,把重点无视掉就不太好了。自己写反射工具类是深入不到VM内部的。

至于“java的反射现在还重吗?试试反射调用10w次花多少时间?”,既然你问,我们就来看看。

这是MethodHandle版,如你所愿调用方法10w次:
import java.dyn.*;

public class SpeedTrap1 {
    private static void doNothing(int x, int y, int z) { }
    
    private static void test(MethodHandle method) {
        for (int i = 0; i < 100000; i++) {
            method.<void>invoke(1, 2, 3);
        }
    }
    
    public static void main(String[] args) {
        MethodHandle method = MethodHandles.lookup()
                              .findStatic(
                                  SpeedTrap1.class,
                                  "doNothing",
                                  MethodType.make(
                                      void.class,
                                      int.class, int.class, int.class));
        // warm up
        for (int i = 0; i < 10; i++) {
            test(method);
        }
        
        // time the test
        long start = System.nanoTime();
        test(method);
        long end = System.nanoTime();
        System.out.println("elapse time: " + (end - start));
    }
}


这是普通的反射版,同样调用方法10w次:
import java.lang.reflect.*;

public class SpeedTrap2 {
    public static void doNothing(int x, int y, int z) { }
    
    private static void test(Method method) throws Throwable {
        for (int i = 0; i < 100000; i++) {
            method.invoke(null, 1, 2, 3);
        }
    }
    
    public static void main(String[] args) throws Throwable {
        Method method = SpeedTrap2.class
                        .getMethod(
                            "doNothing",
                            int.class, int.class, int.class);
        // warm up
        for (int i = 0; i < 10; i++) {
            test(method);
        }
        
        // time the test
        long start = System.nanoTime();
        test(method);
        long end = System.nanoTime();
        System.out.println("elapse time: " + (end - start));
    }
}


代码几乎一模一样,没有作  弊成分。两种方式的测试分开来跑是为了避免前后代码相互干扰。
测试方式是先预热一段时间以确保被测试的test方法被JIT编译,然后再计时跑一次test测试,调用10w次空方法。

在JDK 7 Binary Snapshot build 70上,以Client VM连续测试多次,
MethodHandle版的其中5次测试结果:
引用
elapse time: 2220394
elapse time: 2220673
elapse time: 2226540
elapse time: 2175416
elapse time: 2196648

普通反射版的其中5次测试结果:
引用
elapse time: 22363177
elapse time: 22343343
elapse time: 22353399
elapse time: 22354797
elapse time: 22357311

看清楚了,两组结果的位数不同,时间单位是ns。
目前JDK 7里的JIT编译器还没有为MethodHandle做足优化,内联还没做彻底,况且我用来测试的并不是最新的build。即便如此,消除了反射固有的额外开销就已经有很明显的性能提升。

通过编译日志可以确认两组测试中test方法都确实被编译了:
MethodHandle版:
引用
  1       java.lang.String::hashCode (60 bytes)
  2       java.lang.String::charAt (33 bytes)
  3       java.lang.String::indexOf (151 bytes)
  1%      SpeedTrap1::test @ 2 (22 bytes)
  4       SpeedTrap1::test (22 bytes)

普通反射版:
引用
  1       java.lang.String::hashCode (60 bytes)
  2       java.lang.String::charAt (33 bytes)
  3       java.lang.Object::<init> (1 bytes)
  4       sun.reflect.ClassFileAssembler::emitByte (11 bytes)
  5       sun.reflect.ByteVectorImpl::add (38 bytes)
  6       java.lang.String::indexOf (151 bytes)
  7       java.lang.Integer::valueOf (54 bytes)
  8       java.lang.reflect.Modifier::isPublic (12 bytes)
  9       sun.reflect.Reflection::quickCheckMemberAccess (10 bytes)
---   n   sun.reflect.Reflection::getClassAccessFlags (static)
10  !    java.lang.reflect.Method::invoke (167 bytes)
11       sun.reflect.DelegatingMethodAccessorImpl::invoke (10 bytes)
12  !    sun.reflect.GeneratedMethodAccessor1::invoke (288 bytes)
  1%      SpeedTrap2::test @ 2 (46 bytes)
13       SpeedTrap2::test (46 bytes)

阅读这个日志的方法是:
第一个数字:被JIT编译的方法的序号
%:说明触发了“栈上替换”(on-stack replacement,OSR),
  这意味着被编译的方法是在自身执行过程中被编译的,编译好了之后通过OSR的方式从解释模式转到native模式;
  当一个被OSR方式编译的方法再次被调用时,它就有机会再做一次正常的JIT编译;
!:说明被编译的方法有异常处理块;
n:说明方法是native method;
接下来就是被编译的方法名,
如果后面还有@符号,那就是OSR之后方法的入口在原字节码中的偏移量;
最后的括号是指被编译方法的字节码大小。

由于OSR会阻碍JIT编译器做某些优化,生成的代码效率较差,所以我们希望最后计时的测试是正常JIT编译的。上面的日志说明test方法在OSR方式编译后也做了正常编译,保证测试的稳定性。

OK,那两个版本的test方法被JIT编译后的native code是啥样的呢?下面就来看看
(AT&T语法的x86汇编,代码说明写在注释中。
只保留了正常执行时的代码路径,ret后面接着的异常处理代码省略了):
MethodHandle版:
  ;; 函数入口处理(prologue)
  0x00be8df0: mov    %eax,-0x4000(%esp)
  0x00be8df7: push   %ebp               ; 
  0x00be8df8: mov    %esp,%ebp          ; 上条和这条指令用于建立帧指针
  0x00be8dfa: sub    $0x28,%esp         ; “申请”了0x28字节栈空间,包括局部变量与求值栈
  
  ;; 函数体开始
  0x00be8dfd: mov    %ecx,0x14(%esp)    ; 保护%ecx寄存器,将method暂存到0x14(%esp)处

  ;; 循环初始化
  0x00be8e01: mov    $0x0,%esi          ; int i = 0
  0x00be8e06: jmp    0x00be8e3e         ; 跳转到位于0x00be8e3e的循环条件测试
  0x00be8e0b: nop                       ; 填充一字节无用指令,保证跳转目标在4字节对齐的边界上

  ;; 循环体开始
  0x00be8e0c: mov    %esi,0x10(%esp)    ; %esi寄存器溢出(register spill),将i暂存到0x10(%esp)处
  0x00be8e10: cmp    (%ecx),%eax        ; 隐式空指针检查(看method是否为空),
                                        ; 遇到空指针时会触发访问异常,跳转到0x00be8e50
  0x00be8e12: mov    $0x1,%edx          ; invoke的第一个显式参数,整数1,存入%edx
  0x00be8e17: movl   $0x2,(%esp)        ; invoke的第二个显式参数,整数2,“压入”栈顶
  0x00be8e1e: movl   $0x3,0x4(%esp)     ; invoke的第三个显式参数,整数3,“压入”栈顶+4的位置
  0x00be8e26: mov    %ecx,%edi          ; (这条指令废了……)
  0x00be8e28: mov    %edi,%ecx          ; invoke的隐式参数(method),存入%ecx(原本就在%ecx)
  0x00be8e2a: call   0x00b9af50         ; 调用MethodHandle.invoke方法
  0x00be8e2f: mov    0x10(%esp),%esi    ; 把变量i从0x10(%esp)恢复到%esi
  0x00be8e33: inc    %esi               ; i++
  0x00be8e34: test   %eax,0x990100      ; HotSpot的内部实现细节:{poll}
  0x00be8e3a: mov    0x14(%esp),%ecx    ; 恢复%ecx寄存器的值为method

  ;; 循环条件
  0x00be8e3e: cmp    $0x186a0,%esi      ; i < 100000
  0x00be8e44: jl     0x00be8e0c         ; 如果满足循环条件,跳转回到循环开头(0x00be8e0c)

  ;; 函数出口处理(epilogue)
  0x00be8e46: mov    %ebp,%esp          ;
  0x00be8e48: pop    %ebp               ; 上条和这条指令恢复上一个栈帧
  0x00be8e49: test   %eax,0x990100      ; HotSpot的内部实现细节:{poll_return}
  0x00be8e4f: ret                       ; 返回

MethodHandle版的test方法编译出来相当简洁,其中invoke跟正常的虚方法调用对应的代码一致,没有数组包装,没有原始类型装箱。
这段代码展现了Sun的HotSpot VM在调用函数时使用fastcall calling convention:头两个能被DWORD装下的非浮点参数放在ecx和edx传递,其余参数从右向左压栈(这里没有表现浮点参数的状况)。你可以会觉得“不对啊,上面的代码明明是从左向右处理参数的”,不是先处理了第一个显式参数,然后第二、第三个么?请看清楚,这一系列“压栈”操作并没有使用push指令,而是在不改变esp的前提下向栈顶存入数据。如果改为用push指令,就会先push 0x3再push 0x2,也就是从右向左压栈;这样3才会在2的“下面(地址更大的地方)”。HotSpot C1生成的代码在函数入口处理就申请好了该方法需要的所有栈空间,包括局部变量、临时变量与调用别的函数时压栈用的空间;“Java stack”与“native stack”是融合在一起的。
为了解释代码多废话了几句……OTL
注意:register spill虽然被翻译为“寄存器溢出”,但跟算术溢出“arithmetic overflow”是完全不同的概念,请不要混淆了

普通反射版:
  ;; 函数入口处理(prologue)
  0x00bebab0: mov    %eax,-0x4000(%esp)
  0x00bebab7: push   %ebp
  0x00bebab8: mov    %esp,%ebp
  0x00bebaba: sub    $0x28,%esp         ; “申请”了0x28字节栈空间,包括局部变量与求值栈

  ;; 函数体开始
  0x00bebabd: mov    %ecx,0x18(%esp)

  ;; 循环初始化
  0x00bebac1: mov    $0x0,%esi          ; int i = 0
  0x00bebac6: jmp    0x00bebcab         ; 跳转到位于0x00bebcab的循环条件测试
  0x00bebacb: nop                       ; 填充一字节无用指令,保证跳转目标在4字节对齐的边界上
  ;; 到此为止都基本上跟前一个版本一样

  ;; 循环体开始
  ;; 下面很长一段指令就是创建新的可变长度参数数组,并将原始类型参数装箱存入数组中
  0x00bebacc: mov    %esi,0x1c(%esp)    ; %esi寄存器溢出(register spill),将i暂存到0x1c(%esp)处
  0x00bebad0: mov    $0x3,%ebx          ; 
  0x00bebad5: mov    $0x140acdc0,%edx   ; 将Object[]类型指针存入%edx
  0x00bebada: mov    %ebx,%edi          ; 将整数3存入%edi
  0x00bebadc: cmp    $0xffffff,%ebx     ; 
  0x00bebae2: ja     0x00bebcc1         ; anewarray的慢速路径(现有条件下不会跳转过去)
  0x00bebae8: mov    $0x13,%esi         ; 
  0x00bebaed: lea    (%esi,%ebx,4),%esi ; 将整数31存入%esi
  0x00bebaf0: and    $0xfffffff8,%esi
  0x00bebaf3: mov    %fs:0x0(,%eiz,1),%ecx
  0x00bebafb: mov    -0xc(%ecx),%ecx
  0x00bebafe: mov    0x44(%ecx),%eax
  0x00bebb01: lea    (%eax,%esi,1),%esi
  0x00bebb04: cmp    0x4c(%ecx),%esi
  0x00bebb07: ja     0x00bebcc1
  0x00bebb0d: mov    %esi,0x44(%ecx)
  0x00bebb10: sub    %eax,%esi
  0x00bebb12: movl   $0x1,(%eax)
  0x00bebb18: mov    %edx,0x4(%eax)
  0x00bebb1b: mov    %ebx,0x8(%eax)
  0x00bebb1e: sub    $0xc,%esi
  0x00bebb21: je     0x00bebb64
  0x00bebb27: test   $0x3,%esi
  0x00bebb2d: je     0x00bebb44
  0x00bebb33: push   $0x83989dc         ;   {external_word}
  0x00bebb38: call   0x00bebb3d
  0x00bebb3d: pusha  
  0x00bebb3e: call   0x0801ba80         ;   {runtime_call}
  0x00bebb43: hlt    
  0x00bebb44: xor    %ebx,%ebx
  0x00bebb46: shr    $0x3,%esi
  0x00bebb49: jae    0x00bebb59
  0x00bebb4f: mov    %ebx,0xc(%eax,%esi,8)
  0x00bebb53: je     0x00bebb64
  0x00bebb59: mov    %ebx,0x8(%eax,%esi,8)
  0x00bebb5d: mov    %ebx,0x4(%eax,%esi,8)
  0x00bebb61: dec    %esi
  0x00bebb62: jne    0x00bebb59         ;*anewarray
                                        ; - SpeedTrap2::test@11 (line 8)
  0x00bebb64: mov    %eax,0x20(%esp)
  0x00bebb68: mov    $0x1,%ecx          ; invoke的可变长度参数第一个,整数1
  0x00bebb6d: call   0x00b9b3d0         ; 调用Integer.valueOf完成int的自动装箱
  0x00bebb72: mov    0x20(%esp),%esi
  0x00bebb76: lea    0xc(%esi),%ecx
  0x00bebb79: cmp    $0x0,%eax          ; 检查装箱结果是否为空
  0x00bebb7c: je     0x00bebbbd
  0x00bebb82: mov    0x4(%esi),%edi     ; 隐式空指针检查(看数组是否为空),
                                        ; 遇到空指针时会触发访问异常,跳转到0x00bebccb
  0x00bebb85: mov    0x4(%eax),%ebx
  0x00bebb88: mov    0x88(%edi),%edi
  0x00bebb8e: cmp    %edi,%ebx
  0x00bebb90: je     0x00bebbbd
  0x00bebb96: mov    0x10(%edi),%edx
  0x00bebb99: cmp    (%ebx,%edx,1),%edi
  0x00bebb9c: je     0x00bebbbd
  0x00bebba2: cmp    $0x14,%edx
  0x00bebba5: jne    0x00bebce1
  0x00bebbab: push   %ebx
  0x00bebbac: push   %edi
  0x00bebbad: call   0x00ba9190         ;   {runtime_call}
  0x00bebbb2: pop    %ebx
  0x00bebbb3: pop    %edi
  0x00bebbb4: cmp    $0x0,%edi
  0x00bebbb7: je     0x00bebce1
  0x00bebbbd: mov    %eax,(%ecx)
  0x00bebbbf: shr    $0x9,%ecx
  0x00bebbc2: movb   $0x0,0x2aeff80(%ecx)  ;*aastore
                                        ; - SpeedTrap2::test@20 (line 8)
  0x00bebbc9: mov    $0x2,%ecx          ; invoke的可变长度参数第二个,整数2
  0x00bebbce: call   0x00b9b3d0         ; 调用Integer.valueOf完成int的自动装箱
  0x00bebbd3: mov    0x20(%esp),%esi
  0x00bebbd7: lea    0x10(%esi),%ecx
  0x00bebbda: cmp    $0x0,%eax
  0x00bebbdd: je     0x00bebc1e
  0x00bebbe3: mov    0x4(%esi),%edi     ; 隐式空指针检查(看数组是否为空),
                                        ; 遇到空指针时会触发访问异常,跳转到0x00bebcf7
  0x00bebbe6: mov    0x4(%eax),%ebx
  0x00bebbe9: mov    0x88(%edi),%edi
  0x00bebbef: cmp    %edi,%ebx
  0x00bebbf1: je     0x00bebc1e
  0x00bebbf7: mov    0x10(%edi),%edx
  0x00bebbfa: cmp    (%ebx,%edx,1),%edi
  0x00bebbfd: je     0x00bebc1e
  0x00bebc03: cmp    $0x14,%edx
  0x00bebc06: jne    0x00bebd0d
  0x00bebc0c: push   %ebx
  0x00bebc0d: push   %edi
  0x00bebc0e: call   0x00ba9190         ;   {runtime_call}
  0x00bebc13: pop    %ebx
  0x00bebc14: pop    %edi
  0x00bebc15: cmp    $0x0,%edi
  0x00bebc18: je     0x00bebd0d
  0x00bebc1e: mov    %eax,(%ecx)
  0x00bebc20: shr    $0x9,%ecx
  0x00bebc23: movb   $0x0,0x2aeff80(%ecx)  ;*aastore
                                        ; - SpeedTrap2::test@27 (line 8)
  0x00bebc2a: mov    $0x3,%ecx          ; invoke的可变长度参数第三个,整数3
  0x00bebc2f: call   0x00b9b3d0         ; 调用Integer.valueOf完成int的自动装箱
  0x00bebc34: mov    0x20(%esp),%ecx
  0x00bebc38: lea    0x14(%ecx),%edx
  0x00bebc3b: cmp    $0x0,%eax
  0x00bebc3e: je     0x00bebc7f
  0x00bebc44: mov    0x4(%ecx),%esi     ; 隐式空指针检查(看数组是否为空),
                                        ; 遇到空指针时会触发访问异常,跳转到0x00bebd23
  0x00bebc47: mov    0x4(%eax),%edi
  0x00bebc4a: mov    0x88(%esi),%esi
  0x00bebc50: cmp    %esi,%edi
  0x00bebc52: je     0x00bebc7f
  0x00bebc58: mov    0x10(%esi),%ebx
  0x00bebc5b: cmp    (%edi,%ebx,1),%esi
  0x00bebc5e: je     0x00bebc7f
  0x00bebc64: cmp    $0x14,%ebx
  0x00bebc67: jne    0x00bebd39
  0x00bebc6d: push   %edi
  0x00bebc6e: push   %esi
  0x00bebc6f: call   0x00ba9190         ;   {runtime_call}
  0x00bebc74: pop    %edi
  0x00bebc75: pop    %esi
  0x00bebc76: cmp    $0x0,%esi
  0x00bebc79: je     0x00bebd39
  0x00bebc7f: mov    %eax,(%edx)
  0x00bebc81: shr    $0x9,%edx
  0x00bebc84: movb   $0x0,0x2aeff80(%edx)  ;*aastore
                                        ; - SpeedTrap2::test@34 (line 8)
  0x00bebc8b: mov    0x18(%esp),%esi    ; 将method存入%esi
  0x00bebc8f: cmp    (%esi),%eax        ; 隐式空指针检查(看method是否为空),
                                        ; 遇到空指针时会触发访问异常,跳转到0x00bebd4f
  0x00bebc91: mov    $0x0,%edx          ; invoke的第一个显式参数,null,存入%edx
  0x00bebc96: mov    %ecx,(%esp)        ; invoke的第二个显式参数,可变长度参数数组,“压入”栈顶
  0x00bebc99: mov    %esi,%ecx          ; invoke的隐式参数(method),存入%ecx
  0x00bebc9b: call   0x00b9af50         ; 调用Method.invoke方法
  0x00bebca0: mov    0x1c(%esp),%esi    ; 把变量i从0x1c(%esp)恢复到%esi
  0x00bebca4: inc    %esi               ; i++
  0x00bebca5: test   %eax,0x990100      ; HotSpot的内部实现细节:{poll}

  ;; 循环条件
  0x00bebcab: cmp    $0x186a0,%esi      ; i < 100000
  0x00bebcb1: jl     0x00bebacc         ; 如果满足循环条件,跳转回到循环开头(0x00bebacc)

  ;; 函数出口处理(epilogue)
  0x00bebcb7: mov    %ebp,%esp          ;
  0x00bebcb9: pop    %ebp               ; 上条和这条指令恢复上一个栈帧
  0x00bebcba: test   %eax,0x990100      ; HotSpot的内部实现细节:{poll_return}
  0x00bebcc0: ret                       ; 返回

这个就长了。我们只是想通过反射去调用doNothing()而已,但这里的代码帮我们创建了用于容纳可变长度参数的数组,并且将1、2、3这三个int类型的值装箱为Integer,存入数组,然后再调用Method.invoke;Method.invoke里经过一系列的反射操作,去到一个名为JVM_InvokeMethod的native方法,然后Reflection::invoke_method、Reflection::invoke,把自动装箱的原始类型参数拆箱,接着JavaCalls::call、JavaCalls::call_helper,终于到真正的方法调用点。呼……漫长。

不管MethodHandle.invoke与普通反射的Method.invoke内部的实现,光看test方法的字节码也可以帮助理解上面的包装/装箱状况:
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
        15: iinc          1, 1
        18: goto          2
        21: return       
      LineNumberTable:
        line 7: 0
        line 8: 8
        line 7: 15
        line 10: 21
      StackMapTable: number_of_entries = 2
           frame_type = 252 /* append */
             offset_delta = 2
        locals = [ int ]
           frame_type = 250 /* chop */
          offset_delta = 18

普通反射版:
引用
  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;
        38: pop          
        39: iinc          1, 1
        42: goto          2
        45: return       
      LineNumberTable:
        line 7: 0
        line 8: 8
        line 7: 39
        line 10: 45
      StackMapTable: number_of_entries = 2
           frame_type = 252 /* append */
             offset_delta = 2
        locals = [ int ]
           frame_type = 250 /* chop */
          offset_delta = 42

    Exceptions:
      throws java.lang.Throwable


写了那么长,结论是:如果要说“反射调用不重”,那要看跟什么东西比……

P.S. *有这么一个说法:if you want any framework to look dead slow, make it do nothing, and you'd have to pay all the overhead for no benefits. 这里其实就是如此 ^v^

===========================================================================

补充,新回复:

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功能,所以其开销比接口方法调用还是大很多。不过有两个工程师已经在努力实现相关功能了,可以期待以后的性能改善。
分享到:
评论

相关推荐

    答复: 通过代码简单介绍JDK 7的MethodHandle,并与.NET的委托对比(二)

    标题中的“答复: 通过代码简单介绍JDK 7的MethodHandle,并与.NET的委托对比(二)”表明本文将深入探讨Java中的MethodHandle概念,并将其与.NET平台上的委托进行对比。MethodHandle是JDK 7引入的一个强大特性,它...

    JDK7源代码

    《深入解析JDK7源代码》 JDK7(Java Development Kit 7)是Java编程语言的一个重要版本,它的源代码对于理解Java平台的工作原理、学习面向对象编程以及提升编程技巧具有极大的价值。在这个资源中,包含了 javax、...

    Java 7.0 JDK 7 API CHM part02

    Java 7.0 JDK 7 API CHM part02 本资源有五个压缩包,请全部下载后再解压 下载地址如下: Java 7.0 JDK 7 API CHM part05: http://download.csdn.net/source/3487478 Java 7.0 JDK 7 API CHM part04: ...

    Java 7.0 JDK 7 API CHM part03

    Java 7.0 JDK 7 API CHM part03 本资源有五个压缩包,请全部下载后再解压 下载地址如下: Java 7.0 JDK 7 API CHM part05: http://download.csdn.net/source/3487478 Java 7.0 JDK 7 API CHM part04: ...

    Java 7.0 JDK 7 API CHM part05

    Java 7.0 JDK 7 API CHM part05 本资源有五个压缩包,请全部下载后再解压 下载地址如下: Java 7.0 JDK 7 API CHM part05: http://download.csdn.net/source/3487478 Java 7.0 JDK 7 API CHM part04: ...

    Java 7.0 JDK 7 API CHM part01

    Java 7.0 JDK 7 API CHM part01 本资源有五个压缩包,请全部下载后再解压 下载地址如下: Java 7.0 JDK 7 API CHM part05: http://download.csdn.net/source/3487478 Java 7.0 JDK 7 API CHM part04: ...

    Java 7.0 JDK 7 API CHM part04

    Java 7.0 JDK 7 API CHM part04 本资源有五个压缩包,请全部下载后再解压 下载地址如下: Java 7.0 JDK 7 API CHM part05: http://download.csdn.net/source/3487478 Java 7.0 JDK 7 API CHM part04: ...

    Java帮助文档 JDK全系列 官方中英下载地址.docx

    JDK是由Sun Microsystems(现已被Oracle公司收购)开发并维护的,自Java诞生以来,它一直是Java开发者的首选SDK。本文将深入探讨JDK的主要组件、功能及其不同版本的下载链接。 1. JDK的主要组件: - **javac**:...

    jdk7源代码

    Java Development Kit(JDK)是Java编程语言的核心组件,提供了编写、编译、调试和运行Java应用程序所需的...对比JDK 7与更高版本的源代码,可以帮助开发者追踪语言的发展,理解新特性的实现原理,从而提升编程能力。

    jdk-7u191 (windows和linux)

    标题提到的"jdk-7u191"指的是JDK的第7个主要版本的第191次更新。此版本在发布时是一个重要的里程碑,因为Java 7引入了许多新特性,如动态类型、字符串inswitch、try-with-resources语句等,极大地提高了开发效率。 ...

    javajdk8源码-open-jdk:open-jdk源码jdk7:http://download.java.net/openjdk/jdk

    OpenJDK 8是基于Java SE 8规范的开源实现,它与Oracle JDK 8在功能上非常接近,但OpenJDK是免费且无版权限制的,而Oracle JDK则包含一些闭源的特性和服务。 在OpenJDK 7和8中,有一些重要的新特性和改进: 1. **...

    jdk1.7.0_79下载jdk-7u79-windows-x64

    **Java Development Kit (JDK) 1.7.0_79详解** JDK(Java Development Kit)是Oracle公司提供的用于开发和运行Java应用程序的重要工具集。本文将深入探讨JDK 1.7.0_79版本,该版本特别针对Windows x64操作系统进行...

    jdk-7-windows-x32

    **Java Development Kit (JDK) 7 for Windows 32位详解** JDK(Java Development Kit)是Oracle公司提供的用于开发和运行Java应用程序的核心工具集。JDK 7,也称为Java SE 7 (Java Standard Edition 7),是Java语言...

    jsf-ri.jar from https://javaserverfaces.dev.java.net/

    https://javaserverfaces.dev.java.net/ mojarra-1.2._11_b0_FCS JDK 1.5 绝对能用 good luck

    Jdk7绿色版

    虽然描述中没有明确指出Jdk7绿色版包含MySQL,但是提到了“使用”,这可能意味着该版本提供了一种简便的方法来与MySQL配合,可能是通过预配置的连接驱动或其他工具。 关于JDK7的重要知识点: 1. **多线程改进**:...

    Java JDK 7学习笔记 PDF

    Java JDK 7是Java开发工具包的一个重要版本,它的全称是Java Development Kit,是用于构建和运行Java应用程序的关键组件。...通过这份PDF学习笔记,读者将能深入理解JDK 7的新特性,并能将其应用到实际项目中。

    jdk1.7 64位官方正式版 jdk-7u76-linux-x64.tar.gz

    Java Development Kit (JDK) 1.7 64位官方正式版,即`jdk-7u76-linux-x64.tar.gz`,是Oracle公司为Linux 64位操作系统提供的Java开发工具集。这个版本的JDK对于开发和运行基于Java技术的应用程序至关重要,特别是当...

    jdk-7u80-windows-x64.exe 【官方下载的jdk1.7、jdk7,windows 64位版】

    1. **多语言支持**:Java 7增加了对其他编程语言的支持,如Groovy、Scala等,通过JEP(JDK Enhancement Proposals)292,使得这些语言可以在Java虚拟机(JVM)上运行。 2. **try-with-resources**:这是一个语法糖...

    bcprov-jdk15on-1.68-API文档-中文版.zip

    赠送jar包:bcprov-jdk15on-1.68.jar; 赠送原API文档:bcprov-jdk15on-1.68-javadoc.jar; 赠送源代码:bcprov-jdk15on-1.68-sources.jar; 赠送Maven依赖信息文件:bcprov-jdk15on-1.68.pom; 包含翻译后的API文档...

    JDK7安装包wins.zip

    JDK7,全称为Java SE 7 (Java Platform, Standard Edition 7),是Java平台的一个重大更新,于2011年发布,引入了许多新特性和改进,提升了开发者的工作效率和程序性能。"JDK7win安装版"表明这是专门为Windows操作...

Global site tag (gtag.js) - Google Analytics