java编程中,使用反射来增强灵活性(如各类框架)、某些抽象(如各类框架)及减少样板代码(如Java Bean)。
因此,反射在实际的java项目中被大量使用。
由于项目里存在反射的性能瓶颈,使用的是ReflectASM高性能反射库来优化。
因此,在空闲时间研究了下的这个库,并做了简单的Beachmark。
1. 介绍
ReflectASM是使用字节码生成来加强反射的性能。
反射包含多种反射,这个库很简单,它提供的特性则是:
- 根据匹配的字符串操作成员变量。
- 根据匹配的字符串调用成员函数。
- 根据匹配的字符串调用构造函数。
这三种也恰恰是实际使用中最多的,且在特殊场景下也容易产生性能问题。
2. 例子
举个例子,使用MethodAccess来反射调用类的函数:
更多的例子参考官方文档,这个库本身就不大,就几个类。
3. 实现原理
3.1. MethodAccess.get方法
|
|
大致逻辑为:
- 通过java反射获取必要的函数名、函数类型等信息。
- 动态生成一个用于调用被反射对象的类,其为MethodAccess的子类。
- 反射生成动态生成的类,返回。
由于里面包含字节码生成操作,所以相对来说这个函数是比较耗时的。
我们来分析一下,如果第二次调用对相同的类调用MethodAccess.get()
方法,会不会好一些?
注意到:
因此,如果这个动态生成的MethodAccess类已经生成过,第二次调用MethodAccess.get
是不会操作字节码生成的。
但是,前面的一大堆准备反射信息的操作依然会被执行。所以,如果在代码中封装这样的一个函数试图使用ReflectASM库:
那么每次反射调用前都得执行这么一大坨准备反射信息的代码,实际上还不如用原生反射呢。这个后面会有Beachmark。
为什么不在找不到动态生成的MethodAccess类时(即第一次调用)时,再准备反射信息?这个得问作者。
3.2. 动态生成的类
3.2.1. 通过idea调试器获取动态生成类的字节码
那么那个动态生成的类的内部到底是什么?
由于这个类是动态生成的,所以获取它的定义比较麻烦。
一开始我试图寻找java的ClassLoader的API获取它的字节码,但是似乎没有这种API。
后来,我想了一个办法,直接在MethodAccess.get
里面的这行代码打断点:
通过idea的调试器把data
的内容复制出来。但是这又遇到一个问题,data是二进制内容,根本复制不出来。
一个一年要400美刀的IDE,为啥不能做的贴心一点啊?
既然是二进制内容,那么只能设法将其编码成文本再复制了。通过idea调试器自定义view的功能,将其编码成base64后复制了出来。
然后,搞个python小脚本将其base64解码回.class文件:
反编译.class文件,得到:
可以看到,生成的invoke方法中,直接根据索引使用switch直接调用。
所以,只要使用得当,性能媲美原生调用是没有什么问题的。
3.3. MethodAccess.invoke方法
来看invoke
方法内具体做了哪些操作:
如果通过函数名称调用函数(即调用invoke(Object, String, Class[], Object...)
,
则MethodAccess
是先遍历所有函数名称拿到索引,然后根据索引调用对应方法(即调用虚函数invoke(Object, int, Object...)
,
实际上是通过多态调用字节码动态生成的子类的对应函数。
如果被反射调用的类的函数很多,则这个遍历操作带来的性能损失不能忽略。
所以,性能要求高的场合,应该预先通过getIndex
方法提前获得索引,然后后面即可以直接使用invoke(Object, int, Object...)
来调用。
4. Beachmark
谈这种细粒度操作级别的性能问题,最有说服力的就是实际测试数据了。
下面,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
|
|
可以看到,每次调用都来一次MethodAccess.get
,性能是最慢的,时间消耗是java原生调用的6倍,不如用java原生调用。
最快的则是预先取得MethodAccess和函数的索引并用索引来调用。其时间消耗仅仅是直接调用的2倍不到。
jmh框架十分专业,在基准测试前会做复杂的预热过程以减少环境、优化等影响,基准测试也尽可能通过合理的迭代次数等方式来减小误差。
所以,在默认的迭代次数、预热次数下,跑一次基准测试的时间不短,CPU呼呼的转。。。
5. 最后总结
在使用ReflectASM对某类进行反射调用时,需要预先生成或获取字节码动态生成的MethodAccess子类对象。
这一操作是非常耗时的,所以正确的使用方法应该是:
- 在某个利用反射的耗时函数启动前,先预先生成这个MethodAccess对象。
- 如果是自己里面ReflectASM封装工具类,则应该设计缓存,缓存生成的MethodAccess对象。
如果不这样做,这个ReflectASM用的没有任何意义,性能还不如java的原生反射。
如果想进一步提升性能,那么还应该避免使用函数的字符串名称来调用,而是在耗时的函数启动前,预先获取函数名称对应的整数索引。
在后面的耗时的函数,使用这个整数索引进行调用。