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

原帖在[url]http://www.iteye.com/topic/477934?page=3#1185374[/url],顺手转进来

[quote="star022"]定位到一个java方法,其实只需要类型(Class),方法名及参数即可。[/quote]
对,说得一点也没错,所以MethodHandles的API就是这样的:
[quote]
findStatic(
TestMethodHandle1.class, // 方法所属类型(Class)
"hello", // 方法名
type // 由参数和返回值类型组成的“方法类型”
);
[/quote]
如果只是要做Java的method overload resolution,当然只要参数类型不要返回值类型就够了,但了解class文件及JVM内部数据组织方式的话就会知道,方法的签名(signature)在class文件里是以方法描述符(method descriptor)的形式存在,而该描述符上是有返回值类型的。MethodHandles的API这么设计就是为了快,能更直接的访问VM里的信息,以最快的方式找到目标方法。

[quote="star022"][quote="dennis_zane"][quote="JeffreyZhao"]C#中的委托非常实用,但Java的这个做法有多少可用性啊[/quote]

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


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

这个method handler的使用方式上的确没多少创意,显得还是不够简洁,跟自己写反射工具类使用上区别不大[/quote]
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次测试结果:
[quote]elapse time: 2220394
elapse time: 2220673
elapse time: 2226540
elapse time: 2175416
elapse time: 2196648[/quote]
普通反射版的其中5次测试结果:
[quote]elapse time: 22363177
elapse time: 22343343
elapse time: 22353399
elapse time: 22354797
elapse time: 22357311[/quote]
看清楚了,两组结果的位数不同,时间单位是ns。
目前JDK 7里的JIT编译器还没有为MethodHandle做足优化,内联还没做彻底,况且我用来测试的并不是最新的build。即便如此,消除了反射固有的额外开销就已经有很明显的性能提升。

通过编译日志可以确认两组测试中test方法都确实被编译了:
MethodHandle版:
[quote] 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)[/quote]
普通反射版:
[quote] 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)[/quote]
阅读这个日志的方法是:
第一个数字:被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
([color=red]注意:register spill虽然被翻译为“寄存器溢出”,但跟算术溢出“arithmetic overflow”是完全不同的概念,请不要混淆了[/color])

普通反射版:
  ;; 函数入口处理(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版:
[quote] 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[/quote]
普通反射版:
[quote] 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[/quote]

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

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^

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

补充,新回复:

[quote="JohnnyJian"]反射和MethodHandle差10倍,那MethodHandle和直接的调用差多少?[/quote]
果然要问这个问题么……嘛,测一下也不是不行。
不过仍然需要强调的是,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里最容易优化的一种调用了,所以测试耗时很短:
[quote]elapse time: 134933
elapse time: 134933
elapse time: 134934
elapse time: 134934
elapse time: 135213[/quote]
相比之下,接口方法调用就慢一些,
[quote]elapse time: 469054
elapse time: 468495
elapse time: 475759
elapse time: 468496
elapse time: 468775[/quote]
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功能,所以其开销比接口方法调用还是大很多。不过有两个工程师已经在努力实现相关功能了,可以期待以后的性能改善。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值