newInstance vs new in jdk-9/jdk-8 and jmh

2022-09-01 03:48:57

我在这里看到了很多比较并试图回答哪个更快:或.newInstancenew operator

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

@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

热代码的差异约为2倍,单次拍摄时间的差异要差得多。

现在我切换到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();
}

以下是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    

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


答案 1

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

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

# 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

“已编译为大方法”消息表示编译器无法内联调用,因为被调用方的编译大小大于 value(默认情况下为 2000)。Class.newInstanceInlineSmallCode

当我重新运行基准测试时,它又变得很快。-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

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

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都需要设置一些障碍,这就是为什么编译的代码会变得更大,因此超出了默认限制。编译变得更大的另一个原因是反射代码在JDK 9中稍微重写了一下。Class.newInstanceInlineSmallCodeClass.newInstance

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


答案 2

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

Java 8: Java 9
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;
    }
}
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有一个允许绕过昂贵操作的,例如.我猜这个快速检查已被删除,因为它与新的模块访问规则不兼容。quickCheckMemberAccessReflection.getCallerClass()

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

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

推荐