Java反思:为什么这么慢?

时间:2009-09-08 06:48:49

标签: java performance reflection

我总是根据其缓慢的声誉来避免Java反射。我在当前项目的设计中达到了一个重点,能够使用它会使我的代码更具可读性和优雅性,所以我决定试一试。

我对这种差异感到非常惊讶,有时我注意到运行时间延长了近100倍。即使在这个简单的例子中,它只是实例化一个空类,它也令人难以置信。

class B {

}

public class Test {

    public static long timeDiff(long old) {
        return System.currentTimeMillis() - old;
    }

    public static void main(String args[]) throws Exception {

        long numTrials = (long) Math.pow(10, 7);

        long millis;

        millis = System.currentTimeMillis();

        for (int i=0; i<numTrials; i++) {
            new B();
        }
        System.out.println("Normal instaniation took: "
                 + timeDiff(millis) + "ms");

        millis = System.currentTimeMillis();

        Class<B> c = B.class;

        for (int i=0; i<numTrials; i++) {
            c.newInstance();
        }

        System.out.println("Reflecting instantiation took:" 
              + timeDiff(millis) + "ms");

    }
}

所以,我的问题是

  • 为什么这么慢?有什么我做错了吗? (甚至上面的例子也证明了这一点)。我很难相信它实际上比正常实例慢100倍。

  • 还有什么其他东西可以更好地用于将代码视为数据(请记住,我现在仍然坚持使用Java)

6 个答案:

答案 0 :(得分:42)

由于一些显而易见的原因,反思很慢:

  1. 编译器无法进行任何优化,因为它对您正在做的事情一无所知。这可能适用于JIT
  2. 调用/创建的所有内容都必须发现(即通过名称查找类,查看匹配方法等)
  3. 参数需要通过装箱/取消装箱打包,装入阵列,Exceptions包裹在InvocationTargetException中并重新抛出等。
  4. Jon Skeet mentions here
  5. 的所有处理

    只是因为某些东西慢了100倍并不意味着它太慢了假设反射是你设计程序的“正确方法”。例如,我认为IDE会大量使用反射,从性能角度来看,我的IDE基本上是可以的。

    毕竟,当相比时,反射的开销可能苍白无足轻重,比如解析XML或访问数据库

    要记住的另一点是,微基准是一个众所周知的有缺陷的机制,用于确定某些事物在实践中的速度。与Tim Bender's remarks一样,JVM需要时间来“预热”,JIT可以在运行中重新优化代码热点等。

答案 1 :(得分:39)

您的测试可能存在缺陷。 Generally though the JVM may optimize the normal instantiation but could not make optimizations for the reflective use case

对于那些想知道时代的人,我添加了一个预热阶段并使用数组来维护创建的对象(更类似于真正的程序可能做的事情)。我在我的OSX,jdk7系统上运行了测试代码并得到了这个:

  

反映实例化:5180ms
  正常的实例需要:2001ms

修改测试:

public class Test {

    static class B {

    }

    public static long timeDiff(long old) {
        return System.nanoTime() - old;
    }

    public static void main(String args[]) throws Exception {

        int numTrials = 10000000;
        B[] bees = new B[numTrials];
        Class<B> c = B.class;
        for (int i = 0; i < numTrials; i++) {
            bees[i] = c.newInstance();
        }
        for (int i = 0; i < numTrials; i++) {
            bees[i] = new B();
        }

        long nanos;

        nanos = System.nanoTime();
        for (int i = 0; i < numTrials; i++) {
            bees[i] = c.newInstance();
        }
        System.out.println("Reflecting instantiation took:" + TimeUnit.NANOSECONDS.toMillis(timeDiff(nanos)) + "ms");

        nanos = System.nanoTime();
        for (int i = 0; i < numTrials; i++) {
            bees[i] = new B();
        }
        System.out.println("Normal instaniation took: " + TimeUnit.NANOSECONDS.toMillis(timeDiff(nanos)) + "ms");
    }


}

答案 2 :(得分:27)

用于实例化B的JITted代码令人难以置信的轻量级。基本上它需要分配足够的内存(除非需要GC,否则只是递增指针)而且就是这样 - 没有构造函数代码可以真正调用;我不知道JIT是否会跳过它,但无论如何都没有太多可做的事情。

将其与反射必须做的所有事情进行比较:

  • 检查是否存在无参数构造函数
  • 检查无参数构造函数的可访问性
  • 检查来电者是否有权使用反射
  • 计算(执行时)需要分配多少空间
  • 调用构造函数代码(因为它不会事先知道构造函数为空)

......可能还有其他一些我甚至都没想过的事情。

通常,在性能关键的上下文中不使用反射;如果你需要这样的动态行为,你可以改用BCEL之类的东西。

答案 3 :(得分:10)

似乎如果你使构造函数可访问,它将执行得更快。现在它只比其他版本慢大约10-20倍。

    Constructor<B> c = B.class.getDeclaredConstructor();
    c.setAccessible(true);
    for (int i = 0; i < numTrials; i++) {
        c.newInstance();
    }

Normal instaniation took: 47ms
Reflecting instantiation took:718ms

如果您使用服务器虚拟机,它可以对其进行更多优化,因此速度只会慢3-4倍。这是非常典型的表现。 Geo链接的The article是一个很好的阅读。

Normal instaniation took: 47ms
Reflecting instantiation took:140ms

但是如果你使用-XX:+ DoEscapeAnalysis启用标量替换,那么JVM能够优化常规实例化(它将是0-15ms),但反射实例化保持不变。

答案 4 :(得分:3)

  • 第一次引入时反射速度非常慢,但在较新的JRE中反应速度很快
  • 但是,在内循环中使用反射可能不是一个好主意
  • 基于反射的代码具有基于JIT的优化的低潜力
  • 反射主要用于连接松散耦合的组件,即查找具体的类和方法,其中只有接口是已知的:依赖注入框架,实例化JDBC实现类或XML解析器。这些用途通常可以在系统启动时完成一次,因此效率低下无论如何都无关紧要!

答案 5 :(得分:0)

@Tim Bender的代码在我的机器上提供这些结果(jdk_1.8_45,os_x 10.10,i7,16G):

Reflecting instantiation took:1139ms Normal instaniation took: 4969ms

所以在现代JVM中,反射代码也会很好地优化。