Java反射性能问题

15
我知道这个话题以前已经被讨论过了,例如:Java Reflection Performance,但我的问题是,似乎很多流行的库都是通过注释和反射实现的(例如Gson、Jackson、Jaxb实现、hibernate search等)。即使它们使用反射,许多(如果不是所有)库都提供了良好(或极佳)的性能。我的问题是,它们是如何做到的?是否有一些“诀窍”需要知道,还是它们只是使用直接的反射,而性能上的担忧被夸大了?
编辑: 例如,当我们编写: MyObject obj = new Gson().fromJson(someInputStream, MyObject.class); 我可以理解库可能在内部缓存Field对象,但我觉得它需要每次通过反射实例化对象,并且基于从json解析得到的值,需要调用每个字段(通过反射)的setter方法。 或者说,有没有一种方式只在启动时支付(全部)反射成本?
我确实注意到,Gson/Jackson等具有相对较大的启动成本,在那之后速度非常快。所以显然我想知道,如果我编写一个做类似事情的库,是否有一些技巧需要了解?因为看起来你无法逃避每个调用中的某些反射。

可能是Hibernate实现,我们是否付出了反射代价?的重复问题。 - OscarRyz
6个回答

10

方法查找是昂贵的,但一旦找到要调用的方法,连续的调用就非常相似了。

因此,一旦找到要调用的方法,您只需保留对它的引用,连续的调用就可以类似地工作。

当然,有些情况下您可能想要减少每毫秒的开销。

尽管您应该注意微基准测试,但您可以尝试这个方法来获得一个大致的想法:

import java.lang.reflect.*;
class ReflectionOrNot { 
    public void run() { 
        try { 
            Thread.currentThread().sleep( 0 );
        } catch( InterruptedException ie ){}
    }

    public static void main( String ... args ) throws Exception { 

        ReflectionOrNot ron = new ReflectionOrNot();
        int max = 1000000;

        long start = System.currentTimeMillis();
        for( int i = 0 ; i < max ; i++ ) { 
            ron.run();
        }
        System.out.println( "Direct access took: " + ( System.currentTimeMillis() - start ) );


        Method m = ReflectionOrNot.class.getDeclaredMethod("run");
        start = System.currentTimeMillis();
        for( int i = 0 ; i < max ; i++ ) { 
            m.invoke( ron );
        }
        System.out.println( "Reflection    Took: " + ( System.currentTimeMillis() - start ) );


        start = System.currentTimeMillis();
        for( int i = 0 ; i < max ; i++ ) { 
             m = ReflectionOrNot.class.getDeclaredMethod("run");
            m.invoke( ron );
        }
        System.out.println( "Lookup + Reflect  : " + ( System.currentTimeMillis() - start ) );


    }
}

使用不同方法进行一百万次调用的结果如下:

C:\Users\oreyes\java>java ReflectionOrNot
Direct access took: 422
Reflection    Took: 1156
Lookup + Reflect  : 3016

C:\Users\oreyes\java>java ReflectionOrNot
Direct access took: 422
Reflection    Took: 1125
Lookup + Reflect  : 2750

C:\Users\oreyes\java>java ReflectionOrNot
Direct access took: 485
Reflection    Took: 1203
Lookup + Reflect  : 2797

Class#getDeclaredMethod(s)会复制其结果,这样会不必要地减慢执行时间,而当结果是只读时则更是如此。 - Binkan Salaryman

4

一般而言,并没有什么诀窍。大多数反射行为是在应用程序启动时执行的,因此不会影响启动后的运行时性能。整个hibernate-annotation API就是一个完美的例子。

有时注解的存在需要在整个应用程序生命周期中发挥作用。通常使用动态代理(或例如cglib代理代理具体类)或基于最初反射读取的拦截器进行配置。


Sjoberg:请看一下我上面对问题的编辑。也许您通过关于动态代理(我猜测是cglib)的评论已经回答了我的问题,但我不确定具体怎么操作?也许您可以详细说明或链接到现有的描述? - Kevin
@Kevin,动态代理就像普通代理一样(只是在运行时)-它拦截方法调用并对其进行处理。例如,Spring的@Transactional注释创建一个代理,该代理在目标方法周围创建事务。 - Johan Sjöberg
Sjoberg:好的,这并没有解释像Jackson或Gson这样的库如何避免重复的运行时成本(例如,在我的上面的示例中,Gson每次都需要实例化一个新对象,然后为在某个JSON文档中找到的每个属性调用一个setter(反射))。其他序列化类型库也将面临同样的问题。 - Kevin
@Kevin,将xml转换为对象并不一定需要反射。即使需要反射,与例如通过网络发送流量相比,反射的成本非常。再次强调我的第一个习语,通常没有什么技巧,因为反射并不那么慢。在某些情况下,会使用字节码修改(例如,Hibernate使用cglib来进行延迟加载属性)。 - Johan Sjöberg
@Kevin,例如基准测试动态代理的成本. 他声称使用代理大约慢1.6倍;几乎是零的1.6倍仍然几乎为零。cglib可能同样具有高性能,并允许您注入所需的字节码来补充常规反射。 - Johan Sjöberg

2

与编译器放置的数据相比,反射速度较慢,但与从数据库提供相同数据相比,反射速度较快。

只要应用程序通过反射获取的信息是按需检索并存储在本地缓存中(或以初始化对象的形式),因此在应用程序生命周期内检索只发生一次事件,您就不必担心反射成为性能瓶颈。


1

诀窍在于在“配置时间”使用反射,而不是在“运行时间”使用。

使用反射来检查所需的任何内容,然后将此信息存储(在内存中),以便在运行时使用。


0
我在Windows笔记本电脑上使用Java 8创建了一个包含一百万次迭代和十个填充有随机数字的字段的基准测试。以下是结果:
  • 直接访问:941毫微秒每个对象(参考时间)
  • 朴素内省:4613毫微秒每个对象(+ 390%)
  • 使用HashMap缓存字段的内省:1376毫微秒每个对象(+ 46%)
  • 在本地变量中缓存字段的内省:1105毫微秒每个对象(+ 17%)

如需代码,我可以发布。

对于我来说,如果将所有类和字段查找都排除在循环之外,则内省成本可以忽略不计。

敬礼,


0

只有最初的查找是广泛的,一旦您获得了有关类及其方法的所有所需信息,就没有太大区别。

由于业务层应该长时间运行,因此启动速度较慢,但之后不会有性能损失。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接