一、关于指令调用
1、关于JVM指令调用
无论是基于栈的JVM还是还是基于寄存器的DVM,他们除了操作数栈中变量的移动和空间分配、以及程序计数器的不同,基本方法调用的基本指令都是遵守JSR规范的。
在讨论MethodHandle之前,说一下Java动态性缺陷。Java语言是一门偏向静态的语言,他的动态性一致存在很多局限:
- 泛型缺陷: 前端编译(javac)时泛型被擦除、泛型无法传递、泛型不支持值类型、不支持动态泛型
- 不支持动态方法替换
- 不支持普通类的动态代理
- 生成字节码的工具API相对繁琐等
- 反射调用校验太多需要生成大量字节码,此外需要验证方法区
2、MethodHandle主要作用
MethodHandle主要解决的是问题:
- 反射优化,MethodHandle属于轻量级直接调用字节码,而反射属于重量级,并且完全无法代替反射
- 动态方法分派,能做到子类调用父类的方法,即便这个方法被子类复写了。
- 调用运行在JVM上的其他语言
3、JVM指令调用
我们知道,在JSR标准中,提供了四种调用方法的指令
- invokestatic : 调用静态方法 (JIT 会对这种指令进行内联优化,当然也要看检查静态引用问题,不需要解释器守护内联)
- invokevirtual :调用虚方法,一般指非私有方法 和final方法(这种优化难度较高,JIT会通过 类层次结构分析 (CHA),尝试将方法去虚化,一旦去虚化意味着和invokespacial指令一样调用,最终通过这种方式实现内联、锁优化,这种内联一般需要解释器守护,防止陷阱出现无法逃逸 )
- invokespecial: 在android中,该指令为invokedirect ,调用构造方法和非虚方法 (JIT 会对这种指令进行内联优化,而且不需要解释器守护内联)
- invokeinterface: 调用接口方法(JIT是否优化不取决于这种调用,而是他的实现invokevirtual)
4、MethodHandle的初衷
MethodHandle本质上是为了调用JVM上其他语言,此外也强调方法分派的能力(注意:只能分派直接父类,JSR版本修改之前可以任意分派到父类的父类)
public class MethodHandleDynamicInvoke {
public static void main(String[] args) {
Son son = new Son();
son.love(Father.class);
son.love(Son.class);
}
}
class Father {
String thinking() {
return "Father";
}
}
class Son extends Father {
@Override
String thinking() {
return "Son";
}
public void love(Class target) {
MethodType methodType = MethodType.methodType(String.class);
try {
MethodHandle thinkingMethodHanle = MethodHandles.lookup().findSpecial(target, "thinking", methodType, Son.class).bindTo(Son.this);
System.out.println("I love " + ((String) thinkingMethodHanle.invokeExact()));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
输出结果如下
I love Father
I love Son
二、MethodHandle如何优化反射?
1、MethodHandle相关方法
前一节我们我们加单了解了JVM相关指令和MethodHandle解决的问题,接下来我们介绍以下几个重要的API
- MethodHandle s .lookup() ; 方法查询器
- MethodType.methodType(....); 用于返回值,参数匹配,动态参数,第一个参数是返回值类型,从第二个参数起是方法参数
- MethodHandle.bindTo(Object obj) ;绑定执行对象,绑定之后,invoke活着invokeExact第一个参数就不能传当前对象了
- MethodHanle.invoke(...) ; 方法 参数类型和返回值 必须和MethodType一致,但不同于invokeExact,属于父类的子类不需要转为父类,当然成本是该方法执行效率相当低
- MethodHandle.invokeExact(...) ;方法 参数类型和返回值 必须和MethodType完全一致,如果是String必须转为String,不能用Object 代替,如果是Object类型,必须严格使用Object类型(如 invokeExact((Object)("Hello")) ),必要时进行类型转换,带来的收益是该方法执行效率高
下面给出个例子,展示一下invoke和invokeExact方法的区别
public class Output {
public static MethodHandle getMethodHandle(Object receiver) throws Throwable {
//如果Lookup对象
MethodHandles.Lookup lookup = MethodHandles.lookup();
//MethodType代表方法的类型(不包含方法名称),其实MethodType是为了确定方法的描述符,例如此方法描述符为:(Ljava/lang/Object;)V
MethodType methodType = MethodType.methodType(void.class, Object.class);
//在接收者类中查找一个名为println,指定方法类型的虚方法
return lookup.findVirtual(receiver.getClass(), "println", methodType).bindTo(receiver);
}
public static void main(String[] args) throws Throwable {
Object sysout = System.out;
MethodHandle sysOut = getMethodHandle(sysout);
sysOut.invokeExact((Object) ("Hello Dynamic Invoke : System.out" )); //注意类型转换,否则调用失败
sysOut.invoke("Hello Dynamic Invoke : System.out" );//无需类型转换
Object myOut = new MyOuput();
MethodHandle myOutMethodHandle = getMethodHandle(myOut);
myOutMethodHandle.invokeExact((Object) ("Hello Dynamic Invoke : MyOuput")); //注意类型转换,否则调用失败
myOutMethodHandle.invoke("Hello Dynamic Invoke : MyOuput"); //无需类型转换
}
}
class MyOuput {
public void println(Object value) {
System.out.println("" + value);
}
}
// invoke 动态调用的方法:这个和反射调用基本一致
// invokeExact 调用的方法:类型、返回值类型严格一直,如果参数或者返回值为Object子类,需要强转为Object
输出结果如下
/*
Hello Dynamic Invoke : System.out
Hello Dynamic Invoke : System.out
Hello Dynamic Invoke : MyOuput
Hello Dynamic Invoke : MyOuput
**/
注意:以上我们调用的是没有返回值的println方法,实际上,如果是调用有返回值的的方法,返回值也需要类型转换
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle method = lookup.findVirtual(IName.class,"getName",MethodType.methodType(String.class));
System.out.println("lookup = " +(String)method.invokeExact((IName)(new MyOuput())));
- Lookup findConstructor 、findSpecial 、findVirtual 、findStatic 、findXXXGetter、findXXXSetter ...用于方法查询,其中findSpecial查询非虚方法、findStatic查询静态方法
- Lookup unreflectXXX 配合反射使用,因为MethodHandle并不能完全代替反射,比如调用不同包和class中的私有方法,需要通过反射,然后转为MethodHandle
2、关于性能优化
MethodHandle 如何做到性能优化的?
- MethodHandle静态化
- 提前bindTo性能会更高
- 调用invokeExact
下面我们给出一个例子,定义一个私有方法,提升反射性能
public class ClassMethodHandle {
private int getDoubleVal(int val) {
return val * 2;
}
}
测试代码如下
public class MethodHandleTest {
static Method method_getDoubleVal;
static MethodHandle methodHandle_getDoubleVal;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
method_getDoubleVal = ClassMethodHandle.class.getDeclaredMethod("getDoubleVal",int.class);
method_getDoubleVal.setAccessible(true);
methodHandle_getDoubleVal = lookup.unreflect(method_getDoubleVal);
//私有方法不能通过findSpecial访问,需要借助Method Reflection
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Throwable {
long startTime = System.nanoTime();
testMethodReflection();
long dx1 = System.nanoTime() - startTime;
System.out.println("[1] " + (dx1));
startTime = System.nanoTime();
testMethodHandle();
long dx2 = System.nanoTime() - startTime;
System.out.println("[2] " + (dx2));
System.out.println(">>"+(dx1*1f)/dx2+"<<");
}
private static void testMethodReflection() {
try {
List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
for (int i=0;i<5;i++) {
MethodHandleTest.transformRelection(dataList,method_getDoubleVal,new ClassMethodHandle());
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
private static void testMethodHandle() throws Throwable {
List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
for (int i=0;i<5;i++) {
MethodHandleTest.transform(dataList, methodHandle_getDoubleVal, new ClassMethodHandle());// 方法做为参数
}
}
public static List<Integer> transformRelection(List<Integer> dataList, Method method,Object obj) throws Throwable {
for (int i = 0; i < dataList.size(); i++) {
dataList.set(i, (int) method.invoke(obj,dataList.get(i)));//relect invoke
}
return dataList;
}
public static List<Integer> transform(List<Integer> dataList, MethodHandle handle,Object obj) throws Throwable {
for (int i = 0; i < dataList.size(); i++) {
dataList.set(i, (int) handle.invokeExact((ClassMethodHandle)obj,(int)dataList.get(i)));//注意参数类型转换
}
return dataList;
}
}
性能测试结果如下,取5次中的最大值
[1] 2952626
[2] 1031642
>>2.8620646<<
可见,使用invokeExact的性能是反射的2倍多。
进一步使用bindTo优化
如果提前绑定呢?
public class MethodHandleTest {
static Method method_getDoubleVal;
static MethodHandle methodHandle_getDoubleVal;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
method_getDoubleVal = ClassMethodHandle.class.getDeclaredMethod("getDoubleVal", int.class);
method_getDoubleVal.setAccessible(true);
methodHandle_getDoubleVal = lookup.unreflect(method_getDoubleVal).bindTo(new ClassMethodHandle());
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Throwable {
long startTime = System.nanoTime();
testMethodReflection();
long dx1 = System.nanoTime() - startTime;
System.out.println("[1] " + (dx1));
startTime = System.nanoTime();
testMethodHandle();
long dx2 = System.nanoTime() - startTime;
System.out.println("[2] " + (dx2));
System.out.println(" "+((dx1*1f)/dx2));
}
private static void testMethodReflection() {
try {
ClassMethodHandle classMethodHandle = new ClassMethodHandle();
List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
for (int i = 0; i < 5; i++) {
MethodHandleTest.transformRelection(dataList, method_getDoubleVal, classMethodHandle);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
private static void testMethodHandle() throws Throwable {
List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
for (int i = 0; i < 5; i++) {
MethodHandleTest.transform(dataList, methodHandle_getDoubleVal);// 方法做为参数
}
}
public static List<Integer> transformRelection(List<Integer> dataList, Method method, Object obj) throws Throwable {
for (int i = 0; i < dataList.size(); i++) {
dataList.set(i, (int) method.invoke(obj, dataList.get(i)));//relect invoke
}
return dataList;
}
public static List<Integer> transform(List<Integer> dataList, MethodHandle handle) throws Throwable {
for (int i = 0; i < dataList.size(); i++) {
dataList.set(i, (int) handle.invokeExact((int) dataList.get(i)));//注意参数类型转换
}
return dataList;
}
}
性能测试,取5次中的最大值
[1] 5893674
[2] 1279878
4.6048717
利用静态化的MethodHandle 和bindTo,最终通过invokeExact可以达到4倍左右的性能提升。
三、MethodHandle是否可以用于Android呢?
Android虚拟机和JVM在反射调用的时候有很多区别,DVM移除了很多安全检查和校验,速度本身就很快。我们在Android系统中测试之后,发现使用MethodHandle之后,发现远不如原生反射,因此不推荐在Android系统中使用。