Java 反射性能

Posted

技术标签:

【中文标题】Java 反射性能【英文标题】:Java Reflection Performance 【发布时间】:2010-09-30 22:28:48 【问题描述】:

使用反射创建对象而不是调用类构造函数会导致任何显着的性能差异吗?

【问题讨论】:

相关:Any way to further optimize Java reflective method invocation? 【参考方案1】:

是的 - 绝对。通过反射查找一个类,数量级更昂贵。

引用Java's documentation on reflection:

由于反射涉及到动态解析的类型,某些 Java 虚拟机优化无法执行。因此,反射操作的性能比它们的非反射对应物要慢,并且应该避免在对性能敏感的应用程序中经常调用的代码部分中。

这是我在我的机器上用 5 分钟完成的一个简单测试,运行 Sun JRE 6u10:

public class Main 

    public static void main(String[] args) throws Exception
    
        doRegular();
        doReflection();
    

    public static void doRegular() throws Exception
    
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        
            A a = new A();
            a.doSomeThing();
        
        System.out.println(System.currentTimeMillis() - start);
    

    public static void doReflection() throws Exception
    
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        
            A a = (A) Class.forName("misc.A").newInstance();
            a.doSomeThing();
        
        System.out.println(System.currentTimeMillis() - start);
    

有了这些结果:

35 // no reflection
465 // using reflection

请记住,查找和实例化是一起完成的,在某些情况下可以重构查找,但这只是一个基本示例。

即使您只是实例化,您仍然会受到性能影响:

30 // no reflection
47 // reflection using one lookup, only instantiating

再说一遍,YMMV。

【讨论】:

在我的机器上,只有一个 Class.forName() 调用的 .newInstance() 调用得分为 30 左右。根据 VM 版本,使用适当的缓存策略时,差异可能会比您想象的更接近。 @Peter Lawrey 下面指出这个测试是完全无效的,因为编译器正在优化非反射解决方案(它甚至可以证明什么都没做并优化 for 循环)。需要重新加工,可能应该从 S.O. 中删除。作为不良/误导性信息。在这两种情况下都将创建的对象缓存在数组中,以防止优化器对其进行优化。 (它不能在反射情况下这样做,因为它不能证明构造函数没有副作用) @Bill K - 我们不要得意忘形。是的,由于优化,数字已关闭。不,测试并非完全无效。我添加了一个调用,消除了任何扭曲结果的可能性,并且数字仍然堆积起来以防反射。无论如何,请记住这是一个非常粗略的微基准,它只是表明反射总是会产生一定的开销 这可能是一个无用的基准。取决于 doSomething 的作用。如果它对可见的副作用没有任何作用,那么您的基准测试只运行死代码。 我刚刚见证了 JVM 优化反射 35 倍。在循环中重复运行测试是测试优化代码的方式。第一次迭代:3045ms,第二次迭代:2941ms,第三次迭代:90ms,第四次迭代:83ms。代码:c.newInstance(i)。 c 是构造函数。非反射代码:new A(i),产生 13、4、3.. ms 次。所以是的,在这种情况下,反射很慢,但并不像人们得出的结论那么慢,因为我看到的每个测试,他们只是运行一次测试,而没有给 JVM 机会用机器替换字节码代码。【参考方案2】:

是的,它更慢。

但请记住该死的第一条规则——过早的优化是万恶之源

(好吧,可能与 DRY 的 #1 并列)

我发誓,如果有人在工作中找到我并问我这个问题,我会在接下来的几个月里非常注意他们的代码。

在确定需要它之前,永远不要优化,在此之前,只需编写好的、可读的代码。

哦,我也不是说编写愚蠢的代码。只是考虑你可能做到的最干净的方式——不要复制和粘贴等。(仍然要警惕内部循环之类的东西,并使用最适合你需要的集合——忽略这些并不是“未优化”的编程,这是“糟糕”的编程)

当我听到这样的问题时,我吓坏了,但后来我忘记了每个人都必须自己学习所有规则才能真正掌握它。在你花了一个人月调试某人“优化”的东西后,你会得到它。

编辑:

在这个帖子中发生了一件有趣的事情。检查#1 答案,这是编译器在优化事物方面的强大功能的一个示例。测试完全无效,因为可以完全排除非反射实例化。

上课?在编写干净、代码整洁的解决方案并证明它太慢之前,永远不要优化。

【讨论】:

我完全同意这个回复的观点,但是,如果您即将开始一项重大的设计决策,那么对性能有一个想法会有所帮助,这样您就不会完全不可行小路。也许他只是在做尽职调查? -1:避免以错误的方式做事不是优化,它只是做事。由于实际或虚构的性能问题,优化正在以错误、复杂的方式做事。 @soru 完全同意。为插入排序选择链表而不是数组列表是正确的做法。但是这个特殊的问题——原始问题的双方都有很好的用例,所以根据性能而不是最有用的解决方案来选择一个是错误的。我不确定我们是否完全不同意,所以我不确定你为什么说“-1”。 任何明智的分析师程序员都需要在早期阶段考虑效率,否则您最终可能会得到一个无法在高效且成本低廉的时间框架内优化的系统。不,您不会优化每个时钟周期,但您肯定会为类实例化这样基本的东西采用最佳实践。这个例子是一个很好的例子,为什么你会考虑这些关于反射的问题。这将是一个非常糟糕的程序员,他继续在百万行系统中使用反射,但后来发现它太慢了几个数量级。 @Richard Riley 通常,对于您将使用反射的选定类,类实例化是一个非常罕见的事件。我想你是对的——有些人可能会反思地实例化每个类,即使是那些不断重新创建的类。我会称之为非常糟糕的编程(尽管即使那样你也可以实现类实例的缓存以便事后重用并且不会过多地损害你的代码 - 所以我想我仍然会说总是为了可读性而设计,然后分析和优化稍后)【参考方案3】:

您可能会发现 A a = new A() 正在被 JVM 优化。 如果将对象放入数组中,它们的性能就不会那么好。 ;) 以下打印...

new A(), 141 ns
A.class.newInstance(), 266 ns
new A(), 103 ns
A.class.newInstance(), 261 ns

public class Run 
    private static final int RUNS = 3000000;

    public static class A 
    

    public static void main(String[] args) throws Exception 
        doRegular();
        doReflection();
        doRegular();
        doReflection();
    

    public static void doRegular() throws Exception 
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) 
            as[i] = new A();
        
        System.out.printf("new A(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    

    public static void doReflection() throws Exception 
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) 
            as[i] = A.class.newInstance();
        
        System.out.printf("A.class.newInstance(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    

这表明我的机器上的差异约为 150 ns。

【讨论】:

所以你刚刚杀死了优化器,所以现在两个版本都很慢。因此,反射仍然非常缓慢。 @gbjbaanb 如果优化器正在优化创建本身,那么它不是有效的测试。因此@Peter 的测试是有效的,因为它实际上比较了创建时间(优化器将无法在任何现实世界的情况下工作,因为在任何现实世界的情况下您都需要您正在实例化的对象)。 @nes1983 在这种情况下,您可以借此机会创建一个更好的基准。也许你可以提供一些建设性的东西,比如方法体中应该包含的内容。 在我的 mac,openjdk 7u4 上,差异是 95ns 和 100ns。我没有将 A 存储在数组中,而是存储 hashCodes。如果你说 -verbose:class 你可以看到热点何时生成用于构造 A 的字节码和伴随的加速。 @PeterLawrey 如果我查找一次(一次调用Class.getDeclaredMethod)然后多次调用Method.invoke我使用反射一次还是多次调用它? 后续问题,如果不是Method 而是Constructor 而我多次使用Constructor.newInstance 怎么办?【参考方案4】:

如果真的需要比反射更快的东西,而且这不仅仅是过早的优化,那么使用ASM 或更高级别的库生成字节码是一种选择。第一次生成字节码比只使用反射要慢,但是一旦生成字节码,它就和普通的 Java 代码一样快,并且会被 JIT 编译器优化。

使用代码生成的一些应用示例:

在 CGLIB 生成的代理上调用方法比 Java 的 dynamic proxies 稍快,因为 CGLIB 为其代理生成字节码,但动态代理仅使用反射(I measuredCGLIB 在方法上快 10 倍左右调用,但创建代理较慢)。

JSerial 生成用于读取/写入序列化对象字段的字节码,而不是使用反射。 JSerial 的网站上有some benchmarks。

我不是 100% 确定(而且我现在不想阅读源代码),但我认为 Guice 会生成字节码来进行依赖注入。如果我错了,请纠正我。

【讨论】:

【参考方案5】:

“重要”完全取决于上下文。

如果您使用反射来基于某个配置文件创建单个处理程序对象,然后将其余时间花在运行数据库查询上,那么它就无关紧要了。如果您在紧密循环中通过反射创建大量对象,那么是的,这很重要。

一般来说,设计灵活性(如果需要!)应该推动您使用反射,而不是性能。但是,要确定性能是否是一个问题,您需要进行分析,而不是从讨论论坛中获得任意回复。

【讨论】:

【参考方案6】:

反射有一些开销,但在现代虚拟机上它比以前小了很多。

如果您在程序中使用反射来创建每个简单对象,那么就会出现问题。偶尔使用它,当你有充分的理由时,完全不成问题。

【讨论】:

【参考方案7】:

是的,使用反射时性能会受到影响,但可能的优化解决方法是缓存方法:

  Method md = null;     // Call while looking up the method at each iteration.
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) 
        md = ri.getClass( ).getMethod("getValue", null);
        md.invoke(ri, null);
      

      System.out.println("Calling method " + CALL_AMOUNT+ " times reflexively with lookup took " + (System.currentTimeMillis( ) - millis) + " millis");



      // Call using a cache of the method.

      md = ri.getClass( ).getMethod("getValue", null);
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) 
        md.invoke(ri, null);
      
      System.out.println("Calling method " + CALL_AMOUNT + " times reflexively with cache took " + (System.currentTimeMillis( ) - millis) + " millis");

将导致:

[java] 使用查找自反调用方法 1000000 次耗时 5618 毫秒

[java] 使用缓存自反调用方法 1000000 次耗时 270 毫秒

【讨论】:

重用方法/构造函数确实有用且有帮助,但请注意,由于通常的基准测试问题,上面的测试没有给出有意义的数字(没有预热,所以特别是第一个循环主要是测量 JVM/JIT 预热时间)。【参考方案8】:

有趣的是,设置跳过安全检查的 setAccessible(true) 可以降低 20% 的成本。

没有 setAccessible(true)

new A(), 70 ns
A.class.newInstance(), 214 ns
new A(), 84 ns
A.class.newInstance(), 229 ns

使用 setAccessible(true)

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns

【讨论】:

原则上对我来说似乎很明显。在运行1000000 调用时,这些数字是否呈线性比例? 其实setAccessible()在一般情况下可以有更多的区别,特别是对于具有多个参数的方法,所以应该总是调用它。【参考方案9】:

反射很慢,尽管对象分配并不像反射的其他方面那样无望。使用基于反射的实例化实现同等性能需要您编写代码,以便 jit 可以判断正在实例化哪个类。如果无法确定类的身份,则无法内联分配代码。更糟糕的是,转义分析失败,并且对象无法堆栈分配。如果幸运的话,如果这段代码变热,JVM 的运行时分析可能​​会派上用场,并且可以动态确定哪个类占主导地位并可能针对该类进行优化。

请注意,此线程中的微基准测试存在严重缺陷,因此请谨慎对待。到目前为止,缺陷最少的是 Peter Lawrey 的:它会进行热身运行以使方法受到影响,并且它(有意识地)击败逃逸分析以确保分配实际发生。但是,即使这样也有问题:例如,可以预期大量的数组存储会破坏缓存和存储缓冲区,因此如果您的分配非常快,这将主要成为内存基准。 (感谢彼得得出正确的结论:差异是“150ns”而不是“2.5x”。我怀疑他做这种事情是为了谋生。)

【讨论】:

【参考方案10】:

是的,它明显变慢了。我们正在运行一些执行此操作的代码,虽然我目前没有可用的指标,但最终结果是我们不得不重构该代码以不使用反射。如果你知道类是什么,直接调用构造函数就行了。

【讨论】:

+1 我也有过类似的经历。最好确保仅在绝对必要时使用反射。 例如基于 AOP 的库确实需要反射。【参考方案11】:

在 doReflection() 中是开销,因为 Class.forName("misc.A") (这将需要类查找,可能会扫描 filsystem 上的类路径),而不是调用 newInstance()班级。我想知道如果 Class.forName("misc.A") 只在 for 循环之外执行一次,统计信息会是什么样子,实际上不必为循环的每次调用都执行它。

【讨论】:

【参考方案12】:

是的,通过反射创建对象总是会变慢,因为 JVM 无法在编译时优化代码。有关详细信息,请参阅 Sun/Java Reflection tutorials。

看这个简单的测试:

public class TestSpeed 
    public static void main(String[] args) 
        long startTime = System.nanoTime();
        Object instance = new TestSpeed();
        long endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");

        startTime = System.nanoTime();
        try 
            Object reflectionInstance = Class.forName("TestSpeed").newInstance();
         catch (InstantiationException e) 
            e.printStackTrace();
         catch (IllegalAccessException e) 
            e.printStackTrace();
         catch (ClassNotFoundException e) 
            e.printStackTrace();
        
        endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");
    

【讨论】:

请注意,您应该将查找 (Class.forName()) 与实例化 (newInstance()) 分开,因为它们的性能特征差异很大,您偶尔可以避免在设计良好的情况下重复查找系统。 另外:您需要多次执行每个任务才能获得有用的基准:首先,操作太慢而无法可靠测量,其次您需要预热 HotSpot VM获得有用的数字。【参考方案13】:

您通常可以使用 Apache commons BeanUtils 或 PropertyUtils 来进行自省(基本上它们会缓存有关类的元数据,因此它们并不总是需要使用反射)。

【讨论】:

【参考方案14】:

我认为这取决于目标方法的轻/重。如果目标方法很轻(例如 getter/setter),它可能会慢 1 ~ 3 倍。如果目标方法需要大约 1 毫秒或以上,那么性能将非常接近。这是我使用 Java 8 和 reflectasm 进行的测试:

public class ReflectionTest extends TestCase     
    @Test
    public void test_perf() 
        Profiler.run(3, 100000, 3, "m_01 by refelct", () -> Reflection.on(X.class)._new().invoke("m_01")).printResult();    
        Profiler.run(3, 100000, 3, "m_01 direct call", () -> new X().m_01()).printResult();    
        Profiler.run(3, 100000, 3, "m_02 by refelct", () -> Reflection.on(X.class)._new().invoke("m_02")).printResult();    
        Profiler.run(3, 100000, 3, "m_02 direct call", () -> new X().m_02()).printResult();    
        Profiler.run(3, 100000, 3, "m_11 by refelct", () -> Reflection.on(X.class)._new().invoke("m_11")).printResult();    
        Profiler.run(3, 100000, 3, "m_11 direct call", () -> X.m_11()).printResult();    
        Profiler.run(3, 100000, 3, "m_12 by refelct", () -> Reflection.on(X.class)._new().invoke("m_12")).printResult();    
        Profiler.run(3, 100000, 3, "m_12 direct call", () -> X.m_12()).printResult();
    

    public static class X 
        public long m_01() 
            return m_11();
            
        public long m_02() 
            return m_12();
            
        public static long m_11() 
            long sum = IntStream.range(0, 10).sum();
            assertEquals(45, sum);
            return sum;
            
        public static long m_12() 
            long sum = IntStream.range(0, 10000).sum();
            assertEquals(49995000, sum);
            return sum;
        
    

完整的测试代码在 GitHub:ReflectionTest.java

【讨论】:

以上是关于Java 反射性能的主要内容,如果未能解决你的问题,请参考以下文章

Java各种反射性能对比

ReflectASM => Java 高性能反射

如何利用缓存机制实现JAVA类反射性能提升30倍

如何利用缓存机制实现JAVA类反射性能提升30倍

java反射的性能问题

java反射性能与优化