4年前 (2020-11-09)  Java系列 |   抢沙发  1030 
文章评分 0 次,平均分 0.0

现在您仍然从其他开发人员那里听到的一句话是,Java不仅速度慢,而且反射是JDK中异常缓慢的一部分。这篇文章是为那些相信或希望与信徒进行建设性对话的人准备的。

这篇文章中介绍的技术主题已经有很多年了,但是当你发现它们的时候,有些东西会让你大吃一惊。因此,即使您是一个经验丰富的Java开发人员,您也可以在这里找到一些有趣的地方。

有人怎么能测量出反射是慢的呢?第一个想法是运行一个基准应用程序。这可以简单到如下。

long start = System.nanoTime();
method.invoke("reflection");
long stop = System.nanoTime();
...
long start = System.nanoTime();
"reflection".length();
long stop = System.nanoTime();

结果非常明显:反射版本大约需要15000纳秒,也就是15微秒;方法调用需要2.2微秒,大约快7倍

但是,这真的是一个干净的测试,它向我们展示了真实的结果吗?结果并不完全如此。这项测试远没有做到干净。最明显的一点是,由于可能的类加载、初始化和其他活动,结果将第一次变慢,这可能导致创建过多的对象并调用垃圾回收。

要修复的第一件事是运行测试几次,以便预热代码。你应该做多少次才能得到合理的结果?一、二还是十?事实证明你应该做更多的事情。除了通常的类加载之外,还有一些不同的事情发生在引擎盖下。JIT编译可能在某个时刻发生,也可能发生内部JVM实现优化。事实证明,您可以通过编程方式监视是否有任何类加载和/或JIT编译,以便知道测试中是否存在干扰。这可以通过提供的两个JMX bean来完成。注意,有些JIT编译过程只有在JVM使用-server开关启动时才会触发。默认情况下,JVM在客户机模式下工作,假设比服务器上的随机类调用模式更多,并且不会对代码进行大量优化。因此,如果您曾经在-客户机模式下运行过基准测试,请重做它们,因为它们的差异很大,我们将进一步了解。

ClassLoadingMXBean classLoadingBean = ManagementFactory.getClassLoadingMXBean();
CompilationMXBean compilationBean = ManagementFactory.getCompilationMXBean();

您可以在每次测试迭代之后轮询这些bean,并比较计时以了解在上一次迭代期间类加载或编译是否处于活动状态。好了,这就解决了。现在有更多有趣的话题。在HotSpot JVM中有一种称为反射膨胀的机制。简而言之,它用字节码实现“膨胀”反射方法调用,这意味着调用它们的开销将减少。看看实现情况。默认情况下,只有在15次迭代之后才会触发此过程。这意味着只有第16次测试迭代才能更接近我们在实际应用程序环境中的表现。

现在很明显,第一次尝试进行反射性能测试过于简化,还有很多细节需要考虑。仍然衡量性能的一种方法是使用微基准标记工具。微基准标记工具旨在使JVM在执行测试之前处于“稳定”状态,并运行测试套件足够长的时间,以获得统计上经过验证的结果证据。

我尝试过并非常喜欢的一个框架是brentboyerMicrobenchmarking工具。它有非常简单的API,您唯一应该做的就是将示例代码放在Callable中。

public static void main(String[] args) throws Exception {
    Callable task = new Callable() {
        final Method method;
        final FastMethod fastMethod;
        final Object[] EMPTY_ARRAY = {};
        {
            method = String.class.getMethod("length");
            FastClass fastClass = FastClass.create(String.class);
            fastMethod = fastClass.getMethod(method);
        }

        public Object call() throws Exception {
            //return method.invoke("hello");
            return fastMethod.invoke("hello", EMPTY_ARRAY);
            //return "hello".length();
        }
    };
    Benchmark benchmark = new Benchmark(task);
    System.out.println(benchmark);
}

我在测试中包括了第三种方法调用方法,即CGLib

FastMethod方法。通过使用它,您可以强制从第一次执行时生成字节码方法访问器,并精确地生成所需的位置。这种方法在框架中使用了一段时间(例如在Hibernate中),但是现在人们不再使用这种方法直接调用反射,因为它与现代JVM没有根本的区别。

下面给出了在服务器和客户机模式下运行直接方法调用、反射调用和CGLib FastMethod的结果。

-client getter() :         first = 36.743 us, mean = 10.317 ns (CI deltas: -46.363 ps, +178.344 ps), sd = 3.697 us (CI deltas: -2.959 us, +4.774 us)
-client Method.invoke() :  first = 52.688 us, mean = 128.262 ns (CI deltas: -1.377 ns, +1.635 ns), sd = 17.014 us (CI deltas: -2.323 us, +3.549 us)
-client CgLib FastMethod : first = 42.709 us, mean = 27.511 ns (CI deltas: -119.732 ps, +186.596 ps), sd = 4.722 us (CI deltas: -1.398 us, +2.178 us)

-server getter() :         first = 38.303 us, mean = 3.915 ns (CI deltas: -14.731 ps, +19.287 ps), sd = 1.088 us (CI deltas: -204.310 ns, +360.180 ns)
-server Method.invoke() :  first = 44.177 us, mean = 24.041 ns (CI deltas: -77.722 ps, +124.821 ps), sd = 3.127 us (CI deltas: -925.638 ns, +1.617 us)
-server CgLib FastMethod : first = 44.941 us, mean = 14.756 ns (CI deltas: -97.488 ps, +165.397 ps), sd = 5.685 us (CI deltas: -1.988 us, +2.727 us)

在我的工作站上,通过反射调用一个方法100000(十万)次应该只花费我们大约3毫秒!结论是,在具有适当JVM设置的现代服务器基础设施上,我们不应该太担心refelection性能。另一个结论是,朝着直接字节码操作(如CGLib FastMethod)的方向发展,不会带来性能上的巨大差异,但会增加项目的依赖性和复杂性。如果有人责怪使用反射来检测性能下降,这可能是只需几次迭代的客户端测量的情况。当然,只有当我们简单地比较直接调用和反射调用时,这才有效。如果大量使用类自省API并做很多其他奇特的反射工作,那么事情会变得非常缓慢。

为什么反射慢?

所以如果你不确定你的反射代码慢的程度时可以通过基准测试的方式验证

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/785.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册