Method Handle与反射

如无特殊说明,本文所有代码均基于JDK1.8.0_221

Method Handle入门

反射我们都知道,为我们提供了运行时对类的成员方法访问的手段,极大地提高了Java语言的动态性,但是反射往往意味着效率低下,但是在JDK7以前为了利用反射带来的动态性,我们又不得不使用反射,随着JDK7中新加入的一组API,JDK为我们提供了全新的选择, 也就是Method Handle

什么是Method Handle?

这里引用JDK中的说明

A method handle is a typed, directly executable reference to an underlying method, constructor, field, or similar low-level operation, with optional transformations of arguments or return values.

方法句柄是一个有类型的,可以直接执行的指向底层方法、构造器、field等的引用,可以简单理解为函数指针,它是一种更加底层的查找、调整和调用方法的机制。

如何使用Method Handle?

  • 首先我们需要一个Lookup,lookup是一个创建method handles的工厂,同样引用JDK的说明如下:

A lookup object is a factory for creating method handles,when the creation requires access checking. Method handles do not perform access checks when they are called, but rather when they are created.

Therefore, method handle access restrictions must be enforced when a method handle is created.

上面就其实以及提到了Method handle的不同之处,它的访问检查在创建时就完成了,而发射需要等到调用时,这个等两者对比的时候再说

根据方法修饰符的不同,采用不同的工厂

//访问public方法
MethodHandles.Lookup lookup1 = MethodHandles.publicLookup();
//访问private、protected方法
MethodHandles.Lookup lookup2 = MethodHandles.lookup();
  • 然后我们还需要创建Method Type,它用来描述被访问的方法的参数类型、返回值类型,引用MethodType类的注释如下

A method type represents the arguments and return type accepted and returned by a method handle, or the arguments and return type passed and expected by a method handle caller. Method types must be properly matched between a method handle and all its callers, and the JVM's operations enforce this matching at, specifically during calls to {@link MethodHandle#invokeExact MethodHandle.invokeExact} and {@link MethodHandle#invoke MethodHandle.invoke}, and during execution of {@code invokedynamic} instructions.

可以看到,JVM强制要求声明的Method Type与实际调用方法的参数类型必须匹配。

通过Method Type的静态方法,我们可以非常简单的声明一个Method Type,传入方法的返回值类型和参数类型即可

//以String的length方法为例(void同理,void.class)
MethodType mt = MethodType.methodType(int.class);
  • 再者,通过lookup创建我们的MethodHandle
  1. 访问普通方法
//接上面的length方法
MethodHandle methodHandle = lookup1.findVirtual(String.class, "length", mt);
  1. 访问静态方法
//以valueOf方法为例
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
} MethodType mt2 = MethodType.methodType(String.class,Object.class);
MethodHandle valueOf = lookup1.findStatic(String.class, "valueOf", mt2);
  1. 访问构造函数
MethodType mt3= MethodType.methodType(void.class,String.class);
MethodHandle string = lookup1.findConstructor(String.class, mt3);
  1. 访问私有方法
//以checkBounds方法为例
private static void checkBounds(byte[] bytes, int offset, int length) {
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
} Method checkBounds = String.class.getDeclaredMethod("checkBounds", byte[].class, int.class, int.class);
checkBounds.setAccessible(true);
MethodHandle unreflect = lookup2.unreflect(checkBounds);
unreflect.invoke(new byte[]{},-1,-1);
  1. 访问公有成员

在JDK8中,我还没有找到访问私有成员的方法。

//访问一个自定义类的共有成员
MethodHandle value = lookup2.findGetter(A.class, "value", int.class);
int val = (int)value.invoke(new A(2));
System.out.println(val); static class A{
int value;
A(int value){
this.value=value;
}
}
  • 最后一步就是调用Method Handle了

按照对参数数目、参数类型的要求限制不同,分为三类invokeWithArguments(),invoke(),invokeExact()

  1. invokeWithArguments要求最低,它接收变长参数,允许参数拆装箱类型转换
  2. invoke要求第二,它接收固定的参数列表,允许参拆装箱,类型转换
  3. invokeExact要求最严格,它啥都不允许,参数类型不匹配就报错

示例如下:

//invokeWithArguments,接收变长数组
MethodType mt5 = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = lookup1.findStatic(Arrays.class, "asList", mt5);
List<Integer> integers = (List<Integer>) asList.invokeWithArguments(1, 2);
System.out.println(integers); //invokeExact,直接报错
MethodType mt = MethodType.methodType(int.class, int.class, int.class);
MethodHandle sumMH = lookup1.findStatic(Integer.class, "sum", mt);
int sum = (int) sumMH.invokeExact(1, 1l);
System.out.println(sum); //Exception in thread "main" java.lang.invoke.WrongMethodTypeException: expected (int,int)int but found (int,long)int

Method Handle和反射性能对比

测试程序如下:

package com.hustdj.jdkStudy.methodHandle;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; public class MethodHandleVsReflect {
public static void main(String[] args) {
MethodHandleVsReflect methodHandleVsReflect = new MethodHandleVsReflect();
try {
methodHandleVsReflect.testDirect();
methodHandleVsReflect.testReflect();
methodHandleVsReflect.testMethodHandle();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} public void testDirect(){
A a = new A();
B b = new B();
//预热
for (long i = 0; i < 100_0000_0000L; i++) {
if ((i&1)==0){
a.count(1);
}else{
b.count(1);
}
}
//统计性能
long startNano=System.nanoTime();
for (long i = 0; i < 100_0000_0000L; i++) {
if ((i&1)==0){
a.count(1);
}else{
b.count(1);
}
}
System.out.format("计算结果为: a: %d b: %d",a.i,b.i);
double average = (System.nanoTime() - startNano) / 100_0000_0000.0;
System.out.println("直接调用平均耗时(ns):"+average);
} public void testReflect() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//反射
Method countA = A.class.getMethod("count", Integer.class);
Method countB = B.class.getMethod("count",Integer.class);
A a = new A();
B b = new B();
//预热
for (long i = 0; i < 100_0000_0000L; i++) {
if ((i&1)==1){
countA.invoke(a,1);
}else{
countB.invoke(b,1);
}
}
//统计性能
long startNano=System.nanoTime();
for (long i = 0; i < 100_0000_0000L; i++) {
if ((i&1)==1){
countA.invoke(a,1);
}else{
countB.invoke(b,1);
}
}
System.out.format("计算结果为: a: %d b: %d",a.i,b.i);
double average = (System.nanoTime() - startNano) / 100_0000_0000.0;
System.out.println("反射平均耗时(ns):"+average);
} public void testMethodHandle() throws Throwable {
A a = new A();
B b = new B();
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
MethodType mt = MethodType.methodType(void.class,Integer.class);
MethodHandle countA = publicLookup.findVirtual(A.class, "count", mt);
MethodHandle countB = publicLookup.findVirtual(B.class, "count", mt);
Integer int_1 = new Integer(1);
//预热
for (long i = 0; i < 100_0000_0000L; i++) {
if ((i&1)==1){
countA.invoke(a,1);
}else{
countB.invoke(b,1);
}
}
//统计性能
long startNano=System.nanoTime();
for (long i = 0; i < 100_0000_0000L; i++) {
if ((i&1)==1){
countA.invoke(a,1);
}else{
countB.invoke(b,1);
}
}
System.out.format("计算结果为: a: %d b: %d",a.i,b.i);
double average = (System.nanoTime() - startNano) / 100_0000_0000.0;
System.out.println("methodHandle平均耗时(ns):"+average);
} public class A{
long i=0;
public void count(Integer a){
i++;
}
} public class B{
long i=0;
public void count(Integer a){
i++;
}
}
}

测试结果如下:

//1
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):1.20156184
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):3.05123386
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):7.09741082 //2
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):1.02183322
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):3.44056289
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):6.08221384 //3
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):1.22246659
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):3.41759047
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):5.90614517

通过给JVM添加参数

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

可以发现通过反射调用的invoke已经进行了方法内联

@ 147 java.lang.reflect.Method::invoke (62 bytes) inline (hot)

@ 15 sun.reflect.Reflection::quickCheckMemberAccess (10 bytes) inline (hot)

@ 1 sun.reflect.Reflection::getClassAccessFlags (0 bytes) (intrinsic)

@ 6 java.lang.reflect.Modifier::isPublic (12 bytes) inline (hot)

@ 56 sun.reflect.DelegatingMethodAccessorImpl::invoke (10 bytes) inline (hot)

-> TypeProfile (18366/18366 counts) = sun/reflect/DelegatingMethodAccessorImpl

! @ 6 sun.reflect.GeneratedMethodAccessor2::invoke (66 bytes) inline (hot)

! @ 6 sun.reflect.GeneratedMethodAccessor1::invoke (66 bytes) inline (hot)

-> TypeProfile (5296/10593 counts) = sun/reflect/GeneratedMethodAccessor1

-> TypeProfile (5297/10593 counts) = sun/reflect/GeneratedMethodAccessor2

@ 40 com.hustdj.jdkStudy.methodHandle.MethodHandleVsReflect$A::count (11 bytes) inline (hot)

@ 168 java.lang.reflect.Method::invoke (62 bytes) inline (hot)

@ 15 sun.reflect.Reflection::quickCheckMemberAccess (10 bytes) inline (hot)

@ 1 sun.reflect.Reflection::getClassAccessFlags (0 bytes) (intrinsic)

@ 6 java.lang.reflect.Modifier::isPublic (12 bytes) inline (hot)

@ 56 sun.reflect.DelegatingMethodAccessorImpl::invoke (10 bytes) inline (hot)

-> TypeProfile (18366/18366 counts) = sun/reflect/DelegatingMethodAccessorImpl

! @ 6 sun.reflect.GeneratedMethodAccessor2::invoke (66 bytes) inline (hot)

! @ 6 sun.reflect.GeneratedMethodAccessor1::invoke (66 bytes) inline (hot)

-> TypeProfile (5296/10593 counts) = sun/reflect/GeneratedMethodAccessor1

-> TypeProfile (5297/10593 counts) = sun/reflect/GeneratedMethodAccessor2

@ 40 com.hustdj.jdkStudy.methodHandle.MethodHandleVsReflect$B::count (11 bytes) inline (hot)

难道method handle的性能止步于此了嘛?

不,通过将method handle置为static final的变量,我们甚至可以达到直接调用的效率

static final MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
static final MethodType mt = MethodType.methodType(void.class,Integer.class);
private static final MethodHandle countA=getCountA();
private static final MethodHandle countB=getCountB();
private static MethodHandle getCountA(){
try {
return publicLookup.findVirtual(A.class, "count", mt);
} catch (Throwable e) {
e.printStackTrace();
return null;
}
} private static MethodHandle getCountB(){
try {
return publicLookup.findVirtual(B.class, "count", mt);
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}

调用结果为:

//1
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):0.98582866
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):3.34687653
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):1.17300033
//2
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):0.97544322
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):3.0777855
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):0.95403012
//3
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):1.24535073
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):3.53959802
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):0.97747171

非常的amazing啊,我们简简单单只是从局部变量变成了静态变量,效率直逼直接调用了,为什么呢?

前后对比一下加了static final前的inline信息和不加static final的信息

  • 加入static final的log

com.hustdj.jdkStudy.methodHandle.MethodHandleVsReflect::testMethodHandle @ 33 (200 bytes)

@ 47 java.lang.invoke.LambdaForm$MH/1554547125::invokeExact_MT (21 bytes) force inline by annotation

...

@ 17 com.hustdj.jdkStudy.methodHandle.MethodHandleVsReflect$A::count (11 bytes) inline (hot)

...

@ 17 com.hustdj.jdkStudy.methodHandle.MethodHandleVsReflect$B::count (11 bytes) inline (hot)

我们发现出现了inline,而且这里的内联是直接内联到了最外层的testMethodHandle方法中,区别于method.invoke()的内联

  • 没有static final的log

com.hustdj.jdkStudy.methodHandle.MethodHandleVsReflect::testMethodHandle @ 126 (239 bytes)

@ 140 java.lang.invoke.LambdaForm$MH/1554547125::invokeExact_MT (21 bytes) force inline by annotation

@ 2 java.lang.invoke.Invokers::checkExactType (30 bytes) force inline by annotation

@ 11 java.lang.invoke.MethodHandle::type (5 bytes) accessor

@ 6 java.lang.invoke.Invokers::checkCustomized (20 bytes) force inline by annotation

@ 17 java.lang.invoke.MethodHandle::invokeBasic(LL)V (0 bytes) receiver not constant

@ 151 java.lang.invoke.LambdaForm$MH/1554547125::invokeExact_MT (21 bytes) force inline by annotation

@ 2 java.lang.invoke.Invokers::checkExactType (30 bytes) force inline by annotation

@ 11 java.lang.invoke.MethodHandle::type (5 bytes) accessor

@ 6 java.lang.invoke.Invokers::checkCustomized (20 bytes) force inline by annotation

@ 17 java.lang.invoke.MethodHandle::invokeBasic(LL)V (0 bytes) receiver not constant

并没有看到内联的出现,导致Method Handle的性能大涨的原因找到了,也就是方法内联。

至于为什么会出现这样的情况,这里参考了一下别人的博客https://shipilev.net/jvm/anatomy-quarks/17-trust-nonstatic-final-fields/

虽然他这里提到的是Nostatic final field,但是我们这里是static final field能够直接进行常量替换,不用考虑那么复杂,但是引用博客中的一句话

Constant folding through these final fields is the corner-stone for performance story for MethodHandle-s, VarHandle-s, Atomic*FieldUpdaters` and other high-performance implementations from the core library.

constant folding是MethodHandle-s, VarHandle-s, Atomic*FieldUpdaters`这些高性能实现的性能基石,那method handle可以,反射又行不行呢?测试一下

修改代码如下:

static final Method countAReflect=getReflectA();
static final Method countBReflect=getRelfectB();
private static Method getReflectA(){
try {
return A.class.getMethod("count", Integer.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
return null;
}
}
private static Method getRelfectB(){
try {
return B.class.getMethod("count",Integer.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
return null;
}
}

测试结果为:

//1
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):1.22609891
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):3.43359246
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):0.92919298
//2
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):1.00782253
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):3.60654
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):0.99133146

然而反射并不行!可能要深入JVM才能了解为何Method Handle能做到直接调用的性能吧。

初现端倪

照理说

  • method handle创建时就进行了类型检查,而method.invoke每次调用都需要进行检查
  • method invoke是用数组包装参数的,每次都需要创建一个新的数组
  • method handle在创建之后就是固定的,MH.invoke()自身都可以被内联,而Method.invoke()所有对方法的反射调用都需要经过它,它自身就很难被内联到调用方

但是事实来看method handle的性能很难让人满意大部分情况下都不如反射(除开static final这样的方式),这是为什么呢?

因为JDK8对反射进行了大量的优化,把代码放到JDK7中跑一下结果如下:

//1
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):0.97767317
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):14.81999815
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):10.21145029
//2
计算结果为: a: 10000000000 b: 10000000000直接调用平均耗时(ns):0.97137786
计算结果为: a: 10000000000 b: 10000000000反射平均耗时(ns):14.79272622
计算结果为: a: 10000000000 b: 10000000000methodHandle平均耗时(ns):9.30254589

那个熟悉的慢反射又回来了,在JDK1.7一下,Method Handle确实比反射要快上一些,但是还是比JDK8中慢

Method.invoke()和MethodHandle.invoke()同样是native,为什么反射能够被内联?

JDK的设计者对于Method.invoke()采取了两种策略,一种是native也就是C++的实现方式很难进行内联优化,另一种是在某个方法调用超过阈值后会利用字节码生成技术在内存中生成一个类(暂时没有找到将这个类保存下来的方法),包含要调用的方法,然后加载进虚拟机,这个时候就能内联优化了,而MethodHandle.invoke直接就是native调用,并没有上面的策略,自然也就无法内联,至于设置为static final之后为什么就可以内联了,这个。。。

参考链接

此外,这里再贴一个其他人做的method handle的性能测试

http://chriskirk.blogspot.com/2014/05/which-is-faster-in-java-reflection-or.html

https://www.iteye.com/blog/rednaxelafx-548536

题外话

将循环次数减低到10000,会发现一个奇怪的现象,直接调用居然最慢,感兴趣的可以自行测试一下,会出现made not entrant,JIT会进行反优化,附这个问题的另一个链接https://zhuanlan.zhihu.com/p/82118137

欢迎讨论!!

MethodHandleVS反射的更多相关文章

  1. 隐私泄露杀手锏 —— Flash 权限反射

    [简版:http://weibo.com/p/1001603881940380956046] 前言 一直以为该风险早已被重视,但最近无意中发现,仍有不少网站存在该缺陷,其中不乏一些常用的邮箱.社交网站 ...

  2. Java学习之反射机制及应用场景

    前言: 最近公司正在进行业务组件化进程,其中的路由实现用到了Java的反射机制,既然用到了就想着好好学习总结一下,其实无论是之前的EventBus 2.x版本还是Retrofit.早期的View注解框 ...

  3. 关于 CSS 反射倒影的研究思考

    原文地址:https://css-tricks.com/state-css-reflections 译者:nzbin 友情提示:由于演示 demo 的兼容性,推荐火狐浏览.该文章篇幅较长,内容庞杂,有 ...

  4. 编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议106~109)

    建议106:动态代理可以使代理模式更加灵活 Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发.我们知道一个静态代理是通过主题角色(Prox ...

  5. 运用Mono.Cecil 反射读取.NET程序集元数据

    CLR自带的反射机智和API可以很轻松的读取.NET程序集信息,但是不能对程序集进行修改.CLR提供的是只读的API,但是开源项目Mono.Cecil不仅仅可以读取.NET程序集的元数据,还可以进行修 ...

  6. .NET面试题系列[6] - 反射

    反射 - 定义,实例与优化 在面试中,通常会考察反射的定义(操作元数据),可以用反射做什么(获得程序集及其各个部件),反射有什么使用场景(ORM,序列化,反序列化,值类型比较等).如果答得好,还可能会 ...

  7. .NET基础拾遗(4)委托、事件、反射与特性

    Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...

  8. C++的性能C#的产能?! - .Net Native 系列五:.Net Native与反射

    此系列系小九的学堂原创翻译,翻译自微软官方开发向导,一共分为六个主题.本文是第五个主题:.Net Native与反射. 向导文链接:<C++的性能C#的产能?! - .Net Native 系列 ...

  9. [源码]Literacy 快速反射读写对象属性,字段

    Literacy 说明 Literacy使用IL指令生成方法委托,性能方面,在调用次数达到一定量的时候比反射高很多 当然,用IL指令生成一个方法也是有时间消耗的,所以在只使用一次或少数几次的情况,不但 ...

随机推荐

  1. Android动画系列之属性动画

    原文首发于微信公众号:jzman-blog,欢迎关注交流! 属性动画相较帧动画和补间动画更强大,帧动画和补间动画只能应用于 View 及其子类,而属性动画可以修改任何对象的属性值,属性值可在指定的一段 ...

  2. Spring Boot 2.4发布了,但Spring Cloud用户不推荐着急升级

    前段时间Spring Boot发布了本年度最后一个重要更新版本:Spring Boot 2.4.0. 最近在社群里也开始有讨论关于Spring Boot 2.4的一些使用问题.我发现有很多Spring ...

  3. 重做系统后 恢复oracle 实例

    第一次进行操作,按照网上方法,试了一遍,很多细节搞不清楚,但是还是要记录一下. 备份 old数据库 所用到文件有: XXXXXX\product\11.2.0\dbhome_1\database\pw ...

  4. CenOS下载离线依赖包神器--yumdownloader

    Blog:博客园 个人 本文只总结一些常用的用法,更详细的说明见man yumdownloader和 info yumdownloader. 概述 有时候所在服务器环境不能访问外网,yum安装会感到非 ...

  5. JZOJ 2020.10.6 【NOIP2017提高A组模拟9.7】简单无向图

    简单无向图 题目 Description Input Output Sample Input 输入1: 4 2 1 1 2 输入2: 10 2 2 2 2 1 1 2 1 1 2 Sample Out ...

  6. IAR编译错误Error[e16]: Segment ISTACK (size: 0xc0 align: 0) is too long for segment definition. At least 0x8 more bytes needed. The problem occurred while processing the segment

    问题:个人使用的是IARV9.10编译CC2541的工程,没有做任何修改,直接编译出现如下错误 Error[e16]: Segment ISTACK (size: 0xc0 align: 0) is ...

  7. day7(vue发送短信)

    1.vue发送短信逻辑 前端函数如下,js方法代码无需更改,前端代码逻辑在components\common\lab_header.vue 只需要修改components\axios_api\http ...

  8. Java基础学习之流程控制语句(5)

    目录 1.顺序结构 2.选择结构 2.1.if else结构 2.2.switch case结构 3.循环结构 3.1.while结构 3.2.do while结构 3.3.for结构 3.3.1.普 ...

  9. js 转换为字符串方法

    要把一个值转换为一个字符串有两种方法:toString()方法和转型函数String(). toString()方法 数值.布尔值.对象.字符串值(每个字符串都有一个toString()方法,该方法返 ...

  10. "利用python进行数据分析"学习记录01

    "利用python进行数据分析"学习记录 --day01 08/02 与书相关的资料在 http://github.com/wesm/pydata-book pandas 的2名字 ...