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. 那么多人学习C++,学习它有什么好处?学完以后能从事哪些岗位?

    相信很多人接触编程都是源于大学期间的那堂C++语言程序编程,但是这门课却只告诉了你编程语言是什么,却没告诉你要怎么去熟练掌握编程.所以,不可避免的是许多人在毕业前夕才发现虽然学会了C++,但是好像却不 ...

  2. Kubernetes-20:日志聚合分析系统—Loki的搭建与使用

    日志聚合分析系统--Loki 什么是Loki? Loki 是 Grafana Labs 团队最新的开源项目,是一个水平可扩展,高可用性,多租户的日志聚合系统.它的设计非常经济高效且易于操作,因为它不会 ...

  3. Nginx配置https以及配置说明

    示例 worker_processes 1; events { worker_connections 1024; } http { #均衡负载 upstream demo{ server localh ...

  4. vue----(组件)子组件和父组件

    1.组件的定义 1.定义组件并引用 2.父组件向子组件传值 3.子组件向父组件传值 什么是组件 1.Html中有组件,是一段可以被复用的结构代码 2.Css中有组件,是一段可以被复用的样式 3.Js中 ...

  5. 《高并发下的.NET》第2季 - 故障公告:高并发下全线崩溃

    大家好,非常抱歉,在昨天下午(12月3日)的访问高峰,园子迎来更高的并发,在这样的高并发下,突发的数据库连接故障造成博客站点全线崩溃,由此给您带来很大的麻烦,请您谅解. 最近,我们一边在忙于AWS合作 ...

  6. npm常用操作

    Npm常用操作 1. 淘宝镜像 1.1 npm临时使用淘宝镜像安装依赖包 npm i -g express --registry https://registry.npm.taobao.org 1.2 ...

  7. PyQt(Python+Qt)学习随笔:QTreeWidget树型部件中的QTreeWidgetItem项构造方法

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QTreeWidget树型部件的项是单独的类对象,这个类就是QTreeWidgetItem. QTr ...

  8. selenium常用的标签

    1.selenium之 下拉选择框Select.反选框Deselect.options 我们通常会遇到两种下拉框,一种使用的是html的标签select,另一种是使用input标签做的假下拉框.后者我 ...

  9. csv文件的写操作

    import csv sumbmit_csv_path = "submit_have_valid_SGD.csv" with open(sumbmit_csv_path, &quo ...

  10. JavaScript:常用的一些数组遍历的方法

    常用的一些遍历数组的方法: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...