【问题标题】:newInstance vs new in jdk-9/jdk-8 and jmhjdk-9/jdk-8 和 jmh 中的新实例与新实例
【发布时间】:2017-08-04 19:50:16
【问题描述】:

我在这里看到很多线程比较并尝试回答哪个更快:newInstancenew operator

查看源代码,newInstance 似乎应该慢得多,我的意思是它做了很多安全检查并使用反射。我决定测量,首先运行 jdk-8。这是使用jmh的代码。

@BenchmarkMode(value = { Mode.AverageTime, Mode.SingleShotTime })
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)   
@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)    
@State(Scope.Benchmark) 
public class TestNewObject {
    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder().include(TestNewObject.class.getSimpleName()).build();
        new Runner(opt).run();
    }

    @Fork(1)
    @Benchmark
    public Something newOperator() {
       return new Something();
    }

    @SuppressWarnings("deprecation")
    @Fork(1)
    @Benchmark
    public Something newInstance() throws InstantiationException, IllegalAccessException {
         return Something.class.newInstance();
    }

    static class Something {

    } 
}

我不认为这里有什么大的惊喜(JIT 做了很多优化,使得这种差异没有那么大):

Benchmark                  Mode  Cnt      Score      Error  Units
TestNewObject.newInstance  avgt    5      7.762 ±    0.745  ns/op
TestNewObject.newOperator  avgt    5      4.714 ±    1.480  ns/op
TestNewObject.newInstance    ss    5  10666.200 ± 4261.855  ns/op
TestNewObject.newOperator    ss    5   1522.800 ± 2558.524  ns/op

热代码的差异将在 2x 左右,单次拍摄时间会更差。

现在我切换到 jdk-9(构建 157 以防万一)并运行相同的代码。 结果:

 Benchmark                  Mode  Cnt      Score      Error  Units
 TestNewObject.newInstance  avgt    5    314.307 ±   55.054  ns/op
 TestNewObject.newOperator  avgt    5      4.602 ±    1.084  ns/op
 TestNewObject.newInstance    ss    5  10798.400 ± 5090.458  ns/op
 TestNewObject.newOperator    ss    5   3269.800 ± 4545.827  ns/op

这是热代码的 50 倍 差异。我正在使用最新的 jmh 版本 (1.19.SNAPSHOT)。

在测试中再添加一种方法后:

@Fork(1)
@Benchmark
public Something newInstanceJDK9() throws Exception {
    return Something.class.getDeclaredConstructor().newInstance();
}

这里是 n jdk-9 的总体结果:

TestNewObject.newInstance      avgt    5    308.342 ±   107.563  ns/op
TestNewObject.newInstanceJDK9  avgt    5     50.659 ±     7.964  ns/op
TestNewObject.newOperator      avgt    5      4.554 ±     0.616  ns/op    

谁能解释一下为什么会有这么大的差异

【问题讨论】:

  • 您是否使用带有拼图的 JDK9 构建?
  • 这很重要,因为系统模块会有许多额外的访问检查,JIT 可能还不知道如何很好地处理。
  • Class.newInstance() 在 Java 9 中已弃用。推荐的替代方案 clazz.getDeclaredConstructor().newInstance() 的性能会很有趣……
  • @Holger 好点,补充说。差异仍然是 10x,更好,但距离 2x...
  • 你能做另一个测试吗,现在在Something 中使用非public(默认访问)构造函数?

标签: java performance java-8 jmh java-9


【解决方案1】:

Class.newInstance() 的实现基本相同,除了以下部分:

Java 8:
Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in java.lang.reflect.Constructor)
int modifiers = tmpConstructor.getModifiers();
if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
    Class<?> caller = Reflection.getCallerClass();
    if (newInstanceCallerCache != caller) {
        Reflection.ensureMemberAccess(caller, this, null, modifiers);
        newInstanceCallerCache = caller;
    }
}
Java 9
Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in java.lang.reflect.Constructor)
Class<?> caller = Reflection.getCallerClass();
if (newInstanceCallerCache != caller) {
    int modifiers = tmpConstructor.getModifiers();
    Reflection.ensureMemberAccess(caller, this, null, modifiers);
    newInstanceCallerCache = caller;
}

如您所见,Java 8 有一个quickCheckMemberAccess,它允许绕过昂贵的操作,例如Reflection.getCallerClass()。我猜这个快速检查已被删除,因为它与新的模块访问规则不兼容。

但还有更多。 JVM 可能会使用可预测的类型优化反射实例,Something.class.newInstance() 指的是完全可预测的类型。这种优化可能变得不那么有效了。有几个可能的原因:

  • 新的模块访问规则使过程复杂化
  • 由于 Class.newInstance() 已被弃用,一些支持已被故意删除(在我看来不太可能)
  • 由于上面显示的更改实现代码,HotSpot 无法识别触发优化的某些代码模式

【讨论】:

    【解决方案2】:

    首先,问题与模块系统无关(直接)。

    我注意到,即使使用 JDK 9,newInstance 的第一次预热迭代也与 JDK 8 一样快。

    # Fork: 1 of 1
    # Warmup Iteration   1: 10,578 ns/op    <-- Fast!
    # Warmup Iteration   2: 246,426 ns/op
    # Warmup Iteration   3: 242,347 ns/op
    

    这意味着 JIT 编译出现问题。
    -XX:+PrintCompilation 确认在第一次迭代后重新编译了基准:

    10,762 ns/op
    # Warmup Iteration   2:    1541  689   !   3       java.lang.Class::newInstance (160 bytes)   made not entrant
       1548  692 %     4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub @ 13 (56 bytes)
       1552  693       4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub (56 bytes)
       1555  662       3       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub (56 bytes)   made not entrant
    248,023 ns/op
    

    然后-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining指向内联问题:

    1577  667 %     4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub @ 13 (56 bytes)
                               @ 17   bench.NewInstance::newInstance (6 bytes)   inline (hot)
                !                @ 2   java.lang.Class::newInstance (160 bytes)   already compiled into a big method
    

    “已经编译成一个大方法” 消息意味着编译器未能内联 Class.newInstance 调用,因为被调用者的编译大小大于 InlineSmallCode 值(即 2000 由默认)。

    当我用-XX:InlineSmallCode=2500 重新运行基准测试时,它又变快了。

    Benchmark                Mode  Cnt  Score   Error  Units
    NewInstance.newInstance  avgt    5  8,847 ± 0,080  ns/op
    NewInstance.operatorNew  avgt    5  5,042 ± 0,177  ns/op
    

    您知道,JDK 9 现在将 G1 作为默认 GC。如果我回退到并行 GC,即使使用默认的 InlineSmallCode,基准测试也会很快。

    使用 -XX:+UseParallelGC 重新运行 JDK 9 基准测试:

    Benchmark                Mode  Cnt  Score   Error  Units
    NewInstance.newInstance  avgt    5  8,728 ± 0,143  ns/op
    NewInstance.operatorNew  avgt    5  4,822 ± 0,096  ns/op
    

    G1 需要在对象存储发生时设置一些障碍,这就是编译代码变得有点大的原因,因此Class.newInstance 超过了默认的InlineSmallCode 限制。编译后的Class.newInstance 变大的另一个原因是反射代码在 JDK 9 中略有改写。

    TL;DR JIT 未能内联 Class.newInstance,因为已超出 InlineSmallCode 限制。 Class.newInstance的编译版本变大了,因为JDK 9中反射代码的变化,以及默认GC已经改为G1。

    【讨论】:

    • 这对于像 Spring 这样的反射密集型框架来说不是一个大问题吗?也许这值得报告,以便可以使方法更小(例如,通过将一些代码提取到单独的方法中)。
    • @KirillRakhman 这应该不是问题,因为在现实生活中newInstance 无论如何都不太可能被内联。当 same 构造函数通过反射在 same 位置被调用 多次 时,我无法想象一个合理的情况。在最初的问题中,性能提升只是因为 JIT 适应调用特定方法。
    • 很好的解释,甚至还有 tldr!
    猜你喜欢
    • 1970-01-01
    • 2019-03-07
    • 1970-01-01
    • 2011-07-29
    • 1970-01-01
    • 1970-01-01
    • 2014-05-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多