Java反思:为什么这么慢?

Mik*_*ike 47 java reflection performance

基于其缓慢的声誉,我总是避免使用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");

    }
}
Run Code Online (Sandbox Code Playgroud)

真的,我的问题是

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

  • 是否有其他东西可以更好地用于将代码作为数据处理(请记住,我现在仍然坚持使用Java)

oxb*_*kes 43

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

  1. 编译器无法进行任何优化,因为它无法真正了解您正在做什么.这可能也适用JIT于此
  2. 必须发现所有被调用/创建的东西(即通过名称查找类,查看匹配的方法等)
  3. 参数需要通过装箱/取消装箱打包,装入阵列,Exceptions包裹在InvocationTargetExceptions中并重新抛出等等.
  4. Jon Skeet在这里提到的所有处理.

只是因为某些东西速度慢了100倍并不意味着你觉得反射是你设计程序的"正确方法"的速度太慢.例如,我认为IDE会大量使用反射,从性能角度来看,我的IDE大多数都可以.

毕竟,反射的开销很可能是小巫见大巫相比,比如说,解析XML或访问数据库!

要记住的另一点是,微观基准是一个众所周知的有缺陷的机制,用于确定某些事物在实践中的速度.除了Tim Bender的评论,JVM需要时间来"热身",JIT可以即时重新优化代码热点等.

  • 不能过分强调,与使用等效的非反射代码相比,使用反射的速度很慢.读取或写入HD或解析xml或使用网络或访问数据库(访问HD和网络)都比反射慢. (11认同)
  • 我刚刚目睹了JVM优化反射35倍.在循环中重复运行测试是测试优化代码的方式.第一次迭代:3045ms,第二次迭代:2941ms,第三次迭代:90ms,第四次迭代:83ms.代码:c.newInstance(i).c是一个构造函数.非反射代码:新的A(i),产生13,4,3 ms次.所以是的,在这种情况下,反射很慢,但并不像人们总结的那么慢,因为我看到的每一个测试,他们只是运行测试一次而没有让JVM有机会用机器替换字节代码码. (2认同)

Tim*_*der 40

您的测试可能存在缺陷.通常,虽然JVM可以优化正常实例化但不能对反射用例进行优化.

对于那些想知道时代的人,我添加了一个预热阶段并使用数组来维护创建的对象(更类似于真正的程序可能做的事情).我在我的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");
    }


}
Run Code Online (Sandbox Code Playgroud)

  • 不.我注意到了这种差异,并想知道同样的事情,但选择不去调查.顺便说一句,我发现这是重复:http://stackoverflow.com/questions/435553/java-reflection-performance ...我支持主要答案和Marcus Downing作为第二个答案的贡献. (3认同)
  • 基准测试没有考虑JVM预热/编译阶段.将主内容包装在循环中,您将在后续运行中看到完全不同的数字. (3认同)

Jon*_*eet 27

用于实例化B的JITted代码非常轻量级.基本上它需要分配足够的内存(除非需要GC,否则只是递增指针)而且就是这样 - 没有构造函数代码可以真正调用; 我不知道JIT是否会跳过它,但无论哪种方式都没有.

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

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

......还有其他我甚至都没有想过的事情.

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

  • 如果构造`B`没有做任何事情,它可以完全删除.zOMG,反射速度无限慢! (11认同)

Esk*_*ola 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
Run Code Online (Sandbox Code Playgroud)

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

Normal instaniation took: 47ms
Reflecting instantiation took:140ms
Run Code Online (Sandbox Code Playgroud)

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