java编程中,使用反射来加强灵活性(如各种框架)、某些抽象(如各种框架)及减小样板代码(如Java Bean)。
所以,反射在实际的java项目中被大量使用。java
因为项目里存在反射的性能瓶颈,使用的是ReflectASM高性能反射库来优化。
所以,在空闲时间研究了下的这个库,并作了简单的Beachmark。python
<!--more-->git
ReflectASM是使用字节码生成来增强反射的性能。
反射包含多种反射,这个库很简单,它提供的特性则是:github
这三种也偏偏是实际使用中最多的,且在特殊场景下也容易产生性能问题。编程
举个例子,使用MethodAccess来反射调用类的函数:缓存
Person person = new Person(); MethodAccess m = MethodAccess.get(Person.class); Object value = m.invoke(person, "getName");
更多的例子参考官方文档,这个库自己就不大,就几个类。app
static public MethodAccess get (Class type) { ArrayList<Method> methods = new ArrayList<Method>(); boolean isInterface = type.isInterface(); if (!isInterface) { Class nextClass = type; while (nextClass != Object.class) { addDeclaredMethodsToList(nextClass, methods); nextClass = nextClass.getSuperclass(); } } else { recursiveAddInterfaceMethodsToList(type, methods); } int n = methods.size(); String[] methodNames = new String[n]; Class[][] parameterTypes = new Class[n][]; Class[] returnTypes = new Class[n]; for (int i = 0; i < n; i++) { Method method = methods.get(i); methodNames[i] = method.getName(); parameterTypes[i] = method.getParameterTypes(); returnTypes[i] = method.getReturnType(); } String className = type.getName(); String accessClassName = className + "MethodAccess"; if (accessClassName.startsWith("java.")) accessClassName = "reflectasm." + accessClassName; Class accessClass; AccessClassLoader loader = AccessClassLoader.get(type); synchronized (loader) { try { accessClass = loader.loadClass(accessClassName); } catch (ClassNotFoundException ignored) { String accessClassNameInternal = accessClassName.replace('.', '/'); String classNameInternal = className.replace('.', '/'); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); MethodVisitor mv; /* ... 字节码生成 */ byte[] data = cw.toByteArray(); accessClass = loader.defineClass(accessClassName, data); } } try { MethodAccess access = (MethodAccess)accessClass.newInstance(); access.methodNames = methodNames; access.parameterTypes = parameterTypes; access.returnTypes = returnTypes; return access; } catch (Throwable t) { throw new RuntimeException("Error constructing method access class: " + accessClassName, t); } }
大体逻辑为:框架
因为里面包含字节码生成操做,因此相对来讲这个函数是比较耗时的。
咱们来分析一下,若是第二次调用对相同的类调用MethodAccess.get()
方法,会不会好一些?
注意到:ide
synchronized (loader) { try { accessClass = loader.loadClass(accessClassName); } catch { /* ... */ } }
所以,若是这个动态生成的MethodAccess类已经生成过,第二次调用MethodAccess.get
是不会操做字节码生成的。
可是,前面的一大堆准备反射信息的操做依然会被执行。因此,若是在代码中封装这样的一个函数试图使用ReflectASM库:函数
Object reflectionInvoke(Object bean, String methodName) { MethodAccess m = MethodAccess.get(bean.getClass()); return m.invoke(bean, methodName); }
那么每次反射调用前都得执行这么一大坨准备反射信息的代码,实际上还不如用原生反射呢。这个后面会有Beachmark。
为何不在找不到动态生成的MethodAccess类时(即第一次调用)时,再准备反射信息?这个得问做者。
那么那个动态生成的类的内部究竟是什么?
因为这个类是动态生成的,因此获取它的定义比较麻烦。
一开始我试图寻找java的ClassLoader的API获取它的字节码,可是彷佛没有这种API。
后来,我想了一个办法,直接在MethodAccess.get
里面的这行代码打断点:
byte[] data = cw.toByteArray();
经过idea的调试器把data
的内容复制出来。可是这又遇到一个问题,data是二进制内容,根本复制不出来。
一个一年要400美刀的IDE,为啥不能作的贴心一点啊?
既然是二进制内容,那么只能设法将其编码成文本再复制了。经过idea调试器自定义view的功能,将其编码成base64后复制了出来。
而后,搞个python小脚本将其base64解码回.class文件:
#!/usr/bin/env python3 import base64 with open("tmp.txt", "rb") as fi, open("tmp.class", "wb") as fo: base64.decode(fi, fo)
反编译.class文件,获得:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package io.github.frapples.javademoandcookbook.commonutils.entity; import com.esotericsoftware.reflectasm.MethodAccess; public class PointMethodAccess extends MethodAccess { public PointMethodAccess() { } public Object invoke(Object var1, int var2, Object... var3) { Point var4 = (Point)var1; switch(var2) { case 0: return var4.getX(); case 1: var4.setX((Integer)var3[0]); return null; case 2: return var4.getY(); case 3: var4.setY((Integer)var3[0]); return null; case 4: return var4.toString(); case 5: return Point.of((Integer)var3[0], (Integer)var3[1], (String)var3[2]); default: throw new IllegalArgumentException("Method not found: " + var2); } } }
能够看到,生成的invoke方法中,直接根据索引使用switch直接调用。
因此,只要使用得当,性能媲美原生调用是没有什么问题的。
来看invoke
方法内具体作了哪些操做:
abstract public Object invoke (Object object, int methodIndex, Object... args); /** Invokes the method with the specified name and the specified param types. */ public Object invoke (Object object, String methodName, Class[] paramTypes, Object... args) { return invoke(object, getIndex(methodName, paramTypes), args); } /** Invokes the first method with the specified name and the specified number of arguments. */ public Object invoke (Object object, String methodName, Object... args) { return invoke(object, getIndex(methodName, args == null ? 0 : args.length), args); } /** Returns the index of the first method with the specified name. */ public int getIndex (String methodName) { for (int i = 0, n = methodNames.length; i < n; i++) if (methodNames[i].equals(methodName)) return i; throw new IllegalArgumentException("Unable to find non-private method: " + methodName); }
若是经过函数名称调用函数(即调用invoke(Object, String, Class[], Object...)
,
则MethodAccess
是先遍历全部函数名称拿到索引,而后根据索引调用对应方法(即调用虚函数invoke(Object, int, Object...)
,
其实是经过多态调用字节码动态生成的子类的对应函数。
若是被反射调用的类的函数不少,则这个遍历操做带来的性能损失不能忽略。
因此,性能要求高的场合,应该预先经过getIndex
方法提早得到索引,而后后面便可以直接使用invoke(Object, int, Object...)
来调用。
谈这种细粒度操做级别的性能问题,最有说服力的就是实际测试数据了。
下面,Talk is cheap, show you my beachmark.
首先是相关环境:
操做系统版本: elementary OS 0.4.1 Loki 64-bit
CPU: 双核 Intel® Core™ i5-7200U CPU @ 2.50GHz
JMH基准测试框架版本: 1.21
JVM版本: JDK 1.8.0_181, OpenJDK 64-Bit Server VM, 25.181-b13
Benchmark Mode Cnt Score Error Units // 经过MethodHandle调用。预先获得某函数的MethodHandle ReflectASMBenchmark.javaMethodHandleWithInitGet thrpt 5 122.988 ± 4.240 ops/us // 经过java反射调用。缓存获得的Method对象 ReflectASMBenchmark.javaReflectWithCacheGet thrpt 5 11.877 ± 2.203 ops/us // 经过java反射调用。预先获得某函数的Method对象 ReflectASMBenchmark.javaReflectWithInitGet thrpt 5 66.702 ± 11.154 ops/us // 经过java反射调用。每次调用都先取得Method对象 ReflectASMBenchmark.javaReflectWithOriginGet thrpt 5 3.654 ± 0.795 ops/us // 直接调用 ReflectASMBenchmark.normalCall thrpt 5 1059.926 ± 99.724 ops/us // ReflectASM经过索引调用。预先取得MethodAccess对象,预先取得某函数的索引 ReflectASMBenchmark.reflectAsmIndexWithCacheGet thrpt 5 639.051 ± 47.750 ops/us // ReflectASM经过函数名调用,缓存获得的MethodAccess对象 ReflectASMBenchmark.reflectAsmWithCacheGet thrpt 5 21.868 ± 1.879 ops/us // ReflectASM经过函数名调用,预先获得的MethodAccess ReflectASMBenchmark.reflectAsmWithInitGet thrpt 5 53.370 ± 0.821 ops/us // ReflectASM经过函数名调用,每次调用都取得MethodAccess ReflectASMBenchmark.reflectAsmWithOriginGet thrpt 5 0.593 ± 0.005 ops/us
能够看到,每次调用都来一次MethodAccess.get
,性能是最慢的,时间消耗是java原生调用的6倍,不如用java原生调用。
最快的则是预先取得MethodAccess和函数的索引并用索引来调用。其时间消耗仅仅是直接调用的2倍不到。
基准测试代码见:
https://github.com/frapples/j...
jmh框架十分专业,在基准测试前会作复杂的预热过程以减小环境、优化等影响,基准测试也尽量经过合理的迭代次数等方式来减少偏差。
因此,在默认的迭代次数、预热次数下,跑一次基准测试的时间不短,CPU呼呼的转。。。
在使用ReflectASM对某类进行反射调用时,须要预先生成或获取字节码动态生成的MethodAccess子类对象。
这一操做是很是耗时的,因此正确的使用方法应该是:
若是不这样作,这个ReflectASM用的没有任何意义,性能还不如java的原生反射。
若是想进一步提高性能,那么还应该避免使用函数的字符串名称来调用,而是在耗时的函数启动前,预先获取函数名称对应的整数索引。在后面的耗时的函数,使用这个整数索引进行调用。